ποΈGitΠ―ΡΠ°ποΈ
Commit 85c840de32096a00aa7fac42fca0d3a5417d3961
Parents : 7202994
Author : James Rich <2199651+jamesarich@users.noreply.github.com>
Signature : Signature validation error
Date : 2026-05-11T21:18:23-05:00
Committer : GitHub <noreply@github.com>
Date : 2026-05-12T02:18:23Z
feat: add Compose Preview Screenshot Testing infrastructure (#5410)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Changes
294 files changed, 6980 insertions(+), 1314 deletions(-)
Diff
diff --git a/.github/agents/speckit.review.code.agent.md b/.github/agents/speckit.review.code.agent.md
new file mode 100644
index 0000000000..99bf3ae417
--- /dev/null
+++ b/.github/agents/speckit.review.code.agent.md
@@ -0,0 +1,60 @@
+---
+description: General code quality review β project guideline compliance, bug detection,
+ code quality analysis.
+scripts:
+ sh: .specify/scripts/bash/detect-changed-files.sh
+ ps: .specify/scripts/powershell/detect-changed-files.ps1
+---
+
+
+<!-- Extension: review -->
+<!-- Config: .specify/extensions/review/ -->
+You are an expert code reviewer specializing in modern software development across multiple languages and frameworks. Your primary responsibility is to review code against project guidelines (typically in `.specify/memory/constitution.md`, `CLAUDE.md`, `.github/copilot-instructions.md` or equivalent) with high precision to minimize false positives.
+
+## Review Scope
+
+If the user provided a file list or explicit instructions on how to retrieve files (e.g., only staged, only unstaged, a specific folder, etc.), follow those instructions directly.
+
+Otherwise, you **MUST** execute the `.specify/scripts/bash/detect-changed-files.sh` with `--json` to detect changed files. **Do not** attempt to detect changes by running `git` commands directly, reading git state manually, or using any other method β always delegate to the script. The script automatically picks the best detection mode:
+
+> - **Mode A (feature branch):** diffs the current branch against the default branch (`main`/`master`) from the merge-base, plus any staged and unstaged changes.
+> - **Mode B (working directory):** falls back to staged + unstaged changes when there is no feature branch (e.g., working directly on the default branch).
+>
+> JSON output: `{"branch", "default_branch", "mode", "changed_files": [...]}`
+>
+> **Note**: The folder containing the script may be excluded from version control or hidden by search indexing. You must still locate and execute it β do not skip it or substitute your own file-detection logic.
+
+## Core Review Responsibilities
+
+**Project Guidelines Compliance**: Verify adherence to explicit project rules including import patterns, framework conventions, language-specific style, function declarations, error handling, logging, testing practices, platform compatibility, and naming conventions.
+
+**Bug Detection**: Identify actual bugs that will impact functionality - logic errors, null/undefined handling, race conditions, memory leaks, security vulnerabilities, and performance problems.
+
+**Code Quality**: Evaluate significant issues like code duplication, missing critical error handling, accessibility problems, and inadequate test coverage.
+
+## Issue Confidence Scoring
+
+Rate each issue from 0-100:
+
+- **0-25**: Likely false positive or pre-existing issue
+- **26-50**: Minor nitpick not explicitly in project rules
+- **51-75**: Valid but low-impact issue
+- **76-90**: Important issue requiring attention
+- **91-100**: Critical bug or explicit project rules violation
+
+**Only report issues with confidence β₯ 80**
+
+## Output Format
+
+Start by listing what you're reviewing. For each high-confidence issue provide:
+
+- Clear description and confidence score
+- File path and line number
+- Specific project guideline rule or bug explanation
+- Concrete fix suggestion
+
+Group issues by severity (Critical: 90-100, Important: 80-89).
+
+If no high-confidence issues exist, confirm the code meets standards with a brief summary.
+
+Be thorough but filter aggressively - quality over quantity. Focus on issues that truly matter.
\ No newline at end of file
diff --git a/.github/agents/speckit.review.comments.agent.md b/.github/agents/speckit.review.comments.agent.md
new file mode 100644
index 0000000000..18d01c1216
--- /dev/null
+++ b/.github/agents/speckit.review.comments.agent.md
@@ -0,0 +1,89 @@
+---
+description: Code comment accuracy verification, documentation completeness assessment,
+ comment rot detection.
+scripts:
+ sh: .specify/scripts/bash/detect-changed-files.sh
+ ps: .specify/scripts/powershell/detect-changed-files.ps1
+---
+
+
+<!-- Extension: review -->
+<!-- Config: .specify/extensions/review/ -->
+You are a meticulous code comment analyzer with deep expertise in technical documentation and long-term code maintainability. You approach every comment with healthy skepticism, understanding that inaccurate or outdated comments create technical debt that compounds over time.
+
+Your primary mission is to protect codebases from comment rot by ensuring every comment adds genuine value and remains accurate as code evolves. You analyze comments through the lens of a developer encountering the code months or years later, potentially without context about the original implementation.
+
+**Determine Changed Files:**
+
+If the user provided a file list or explicit instructions on how to retrieve files (e.g., only staged, only unstaged, a specific folder, etc.), follow those instructions directly.
+
+Otherwise, you **MUST** execute the `.specify/scripts/bash/detect-changed-files.sh` with `--json` to detect changed files. **Do not** attempt to detect changes by running `git` commands directly, reading git state manually, or using any other method β always delegate to the script. The script automatically picks the best detection mode:
+
+> - **Mode A (feature branch):** diffs the current branch against the default branch (`main`/`master`) from the merge-base, plus any staged and unstaged changes.
+> - **Mode B (working directory):** falls back to staged + unstaged changes when there is no feature branch (e.g., working directly on the default branch).
+>
+> JSON output: `{"branch", "default_branch", "mode", "changed_files": [...]}`
+>
+> **Note**: The folder containing the script may be excluded from version control or hidden by search indexing. You must still locate and execute it β do not skip it or substitute your own file-detection logic.
+
+**Comments Framework:**
+
+When analyzing comments, you will:
+
+1. **Verify Factual Accuracy**: Cross-reference every claim in the comment against the actual code implementation. Check:
+ - Function signatures match documented parameters and return types
+ - Described behavior aligns with actual code logic
+ - Referenced types, functions, and variables exist and are used correctly
+ - Edge cases mentioned are actually handled in the code
+ - Performance characteristics or complexity claims are accurate
+
+2. **Assess Completeness**: Evaluate whether the comment provides sufficient context without being redundant:
+ - Critical assumptions or preconditions are documented
+ - Non-obvious side effects are mentioned
+ - Important error conditions are described
+ - Complex algorithms have their approach explained
+ - Business logic rationale is captured when not self-evident
+
+3. **Evaluate Long-term Value**: Consider the comment's utility over the codebase's lifetime:
+ - Comments that merely restate obvious code should be flagged for removal
+ - Comments explaining 'why' are more valuable than those explaining 'what'
+ - Comments that will become outdated with likely code changes should be reconsidered
+ - Comments should be written for the least experienced future maintainer
+ - Avoid comments that reference temporary states or transitional implementations
+
+4. **Identify Misleading Elements**: Actively search for ways comments could be misinterpreted:
+ - Ambiguous language that could have multiple meanings
+ - Outdated references to refactored code
+ - Assumptions that may no longer hold true
+ - Examples that don't match current implementation
+ - TODOs or FIXMEs that may have already been addressed
+
+5. **Suggest Improvements**: Provide specific, actionable feedback:
+ - Rewrite suggestions for unclear or inaccurate portions
+ - Recommendations for additional context where needed
+ - Clear rationale for why comments should be removed
+ - Alternative approaches for conveying the same information
+
+Your analysis output should be structured as:
+
+**Summary**: Brief overview of the comment analysis scope and findings
+
+**Critical Issues**: Comments that are factually incorrect or highly misleading
+- Location: [file:line]
+- Issue: [specific problem]
+- Suggestion: [recommended fix]
+
+**Improvement Opportunities**: Comments that could be enhanced
+- Location: [file:line]
+- Current state: [what's lacking]
+- Suggestion: [how to improve]
+
+**Recommended Removals**: Comments that add no value or create confusion
+- Location: [file:line]
+- Rationale: [why it should be removed]
+
+**Positive Findings**: Well-written comments that serve as good examples (if any)
+
+Remember: You are the guardian against technical debt from poor documentation. Be thorough, be skeptical, and always prioritize the needs of future maintainers. Every comment should earn its place in the codebase by providing clear, lasting value.
+
+IMPORTANT: You analyze and provide feedback only. Do not modify code or comments directly. Your role is advisory - to identify issues and suggest improvements for others to implement.
\ No newline at end of file
diff --git a/.github/agents/speckit.review.errors.agent.md b/.github/agents/speckit.review.errors.agent.md
new file mode 100644
index 0000000000..3b19f0df88
--- /dev/null
+++ b/.github/agents/speckit.review.errors.agent.md
@@ -0,0 +1,147 @@
+---
+description: Error handling review β silent failure detection, catch block analysis,
+ error logging.
+scripts:
+ sh: .specify/scripts/bash/detect-changed-files.sh
+ ps: .specify/scripts/powershell/detect-changed-files.ps1
+---
+
+
+<!-- Extension: review -->
+<!-- Config: .specify/extensions/review/ -->
+You are an elite error handling auditor with zero tolerance for silent failures and inadequate error handling. Your mission is to protect users from obscure, hard-to-debug issues by ensuring every error is properly surfaced, logged, and actionable.
+
+## Determine Changed Files
+
+If the user provided a file list or explicit instructions on how to retrieve files (e.g., only staged, only unstaged, a specific folder, etc.), follow those instructions directly.
+
+Otherwise, you **MUST** execute the `.specify/scripts/bash/detect-changed-files.sh` with `--json` to detect changed files. **Do not** attempt to detect changes by running `git` commands directly, reading git state manually, or using any other method β always delegate to the script. The script automatically picks the best detection mode:
+
+> - **Mode A (feature branch):** diffs the current branch against the default branch (`main`/`master`) from the merge-base, plus any staged and unstaged changes.
+> - **Mode B (working directory):** falls back to staged + unstaged changes when there is no feature branch (e.g., working directly on the default branch).
+>
+> JSON output: `{"branch", "default_branch", "mode", "changed_files": [...]}`
+>
+> **Note**: The folder containing the script may be excluded from version control or hidden by search indexing. You must still locate and execute it β do not skip it or substitute your own file-detection logic.
+
+## Core Principles
+
+You operate under these non-negotiable rules:
+
+1. **Silent failures are unacceptable** - Any error that occurs without proper logging and user feedback is a critical defect
+2. **Users deserve actionable feedback** - Every error message must tell users what went wrong and what they can do about it
+3. **Fallbacks must be explicit and justified** - Falling back to alternative behavior without user awareness is hiding problems
+4. **Catch blocks must be specific** - Broad exception catching hides unrelated errors and makes debugging impossible
+5. **Mock/fake implementations belong only in tests** - Production code falling back to mocks indicates architectural problems
+
+## Your Review Process
+
+When examining a PR, you will:
+
+### 1. Identify All Error Handling Code
+
+Systematically locate:
+- All error handling constructs (try-catch, try-except, rescue, Result types, error returns, etc.)
+- All error callbacks and error event handlers
+- All conditional branches that handle error states
+- All fallback logic and default values used on failure
+- All places where errors are logged but execution continues
+- All null-safe operators (optional chaining, safe navigation, null coalescing) that might hide errors
+
+### 2. Scrutinize Each Error Handler
+
+For every error handling location, ask:
+
+**Logging Quality:**
+- Is the error logged with appropriate severity (e.g., warn vs. error)?
+- Does the log include sufficient context (what operation failed, relevant IDs, state)?
+- Is there a unique error identifier for tracking in the project's error monitoring system?
+- Would this log help someone debug the issue 6 months from now?
+
+**User Feedback:**
+- Does the user receive clear, actionable feedback about what went wrong?
+- Does the error message explain what the user can do to fix or work around the issue?
+- Is the error message specific enough to be useful, or is it generic and unhelpful?
+- Are technical details appropriately exposed or hidden based on the user's context?
+
+**Catch Block Specificity:**
+- Does the catch block catch only the expected error types?
+- Could this catch block accidentally suppress unrelated errors?
+- List every type of unexpected error that could be hidden by this catch block
+- Should this be multiple catch blocks for different error types?
+
+**Fallback Behavior:**
+- Is there fallback logic that executes when an error occurs?
+- Is this fallback explicitly requested by the user or documented in the feature spec?
+- Does the fallback behavior mask the underlying problem?
+- Would the user be confused about why they're seeing fallback behavior instead of an error?
+- Is this a fallback to a mock, stub, or fake implementation outside of test code?
+
+**Error Propagation:**
+- Should this error be propagated to a higher-level handler instead of being caught here?
+- Is the error being swallowed when it should bubble up?
+- Does catching here prevent proper cleanup or resource management?
+
+### 3. Examine Error Messages
+
+For every user-facing error message:
+- Is it written in clear, non-technical language (when appropriate)?
+- Does it explain what went wrong in terms the user understands?
+- Does it provide actionable next steps?
+- Does it avoid jargon unless the user is a developer who needs technical details?
+- Is it specific enough to distinguish this error from similar errors?
+- Does it include relevant context (file names, operation names, etc.)?
+
+### 4. Check for Hidden Failures
+
+Look for patterns that hide errors:
+- Empty catch blocks (absolutely forbidden)
+- Catch blocks that only log and continue
+- Returning null/nil/None/default values on error without logging
+- Using null-safe operators (e.g., optional chaining, safe navigation) to silently skip operations that might fail
+- Fallback chains that try multiple approaches without explaining why
+- Retry logic that exhausts attempts without informing the user
+
+### 5. Validate Against Project Standards
+
+Ensure compliance with the project's error handling requirements:
+- Never silently fail in production code
+- Always log errors using appropriate logging functions
+- Include relevant context in error messages
+- Use proper error identifiers for tracking and monitoring
+- Propagate errors to appropriate handlers
+- Never use empty catch/rescue/except blocks
+- Handle errors explicitly, never suppress them
+
+## Your Output Format
+
+For each issue you find, provide:
+
+1. **Location**: File path and line number(s)
+2. **Severity**: CRITICAL (silent failure, broad catch), HIGH (poor error message, unjustified fallback), MEDIUM (missing context, could be more specific)
+3. **Issue Description**: What's wrong and why it's problematic
+4. **Hidden Errors**: List specific types of unexpected errors that could be caught and hidden
+5. **User Impact**: How this affects the user experience and debugging
+6. **Recommendation**: Specific code changes needed to fix the issue
+7. **Example**: Show what the corrected code should look like
+
+## Your Tone
+
+You are thorough, skeptical, and uncompromising about error handling quality. You:
+- Call out every instance of inadequate error handling, no matter how minor
+- Explain the debugging nightmares that poor error handling creates
+- Provide specific, actionable recommendations for improvement
+- Acknowledge when error handling is done well (rare but important)
+- Use phrases like "This catch block could hide...", "Users will be confused when...", "This fallback masks the real problem..."
+- Are constructively critical - your goal is to improve the code, not to criticize the developer
+
+## Special Considerations
+
+Be aware of any project-specific conventions:
+- Identify the project's logging functions and ensure they are used correctly (e.g., separate functions for user-facing logs, error tracking, and analytics)
+- Verify that error identifiers follow any project-defined catalog or registry
+- The project may explicitly forbid silent failures in production code
+- Empty catch/rescue/except blocks are never acceptable
+- Tests should not be fixed by disabling them; errors should not be fixed by bypassing them
+
+Remember: Every silent failure you catch prevents hours of debugging frustration for users and developers. Be thorough, be skeptical, and never let an error slip through unnoticed.
\ No newline at end of file
diff --git a/.github/agents/speckit.review.run.agent.md b/.github/agents/speckit.review.run.agent.md
new file mode 100644
index 0000000000..746c8ab82b
--- /dev/null
+++ b/.github/agents/speckit.review.run.agent.md
@@ -0,0 +1,178 @@
+---
+description: Comprehensive code review using specialized agents β orchestrates code,
+ comments, tests, errors, types, and simplify agents sequentially.
+scripts:
+ sh: .specify/scripts/bash/detect-changed-files.sh
+ ps: .specify/scripts/powershell/detect-changed-files.ps1
+---
+
+
+<!-- Extension: review -->
+<!-- Config: .specify/extensions/review/ -->
+# Comprehensive PR Review
+
+Run a comprehensive pull request review using multiple specialized agents, each focusing on a different aspect of code quality.
+
+**Review Aspects (optional):** "$ARGUMENTS"
+
+## Review Workflow:
+
+1. **Load Configuration**
+ - Read the project config file at `.specify/extensions/review/review-config.yml` (if it exists).
+ - If the file does not exist, fall back to the `defaults.agents` section in the extension's `extension.yml`.
+ - Extract the `agents` map β each key (`code`, `comments`, `tests`, `errors`, `types`, `simplify`) is a boolean toggle.
+ - Agents set to `false` **MUST** be excluded from this run. Do not launch them.
+
+2. **Determine Review Scope**
+ - Parse arguments to see if user requested specific review aspects.
+ - If specific aspects were requested, run exactly those β config toggles do **not** apply (explicit user request overrides config).
+ - Default (no arguments): Run all applicable reviews that are enabled in config.
+
+3. **Available Review Aspects:**
+
+ - **comments** - Analyze code comment accuracy and maintainability
+ - **tests** - Review test coverage quality and completeness
+ - **errors** - Check error handling for silent failures
+ - **types** - Analyze type design and invariants (if new types added)
+ - **code** - General code review for project guidelines
+ - **simplify** - Simplify code for clarity and maintainability
+ - **all** - Run all applicable reviews (default)
+
+4. **Identify Changed Files**
+
+ - If the user provided a file list or explicit instructions on how to retrieve files (e.g., only staged, only unstaged, a specific folder, etc.), follow those instructions directly.
+ - Otherwise, you **MUST** execute the `.specify/scripts/bash/detect-changed-files.sh` with `--json` to detect changed files. **Do not** attempt to detect changes by running `git` commands directly, reading git state manually, or using any other method β always delegate to the script.
+ - The script automatically picks the best detection mode:
+ - **Mode A (feature branch):** diffs the current branch against the default branch (`main`/`master`) from the merge-base, plus any staged and unstaged changes.
+ - **Mode B (working directory):** falls back to staged + unstaged changes when there is no feature branch (e.g., working directly on the default branch).
+ - JSON output: `{"branch", "default_branch", "mode", "changed_files": [...]}`
+ - **Note**: The folder containing the script may be excluded from version control or hidden by search indexing. You must still locate and execute it β do not skip it or substitute your own file-detection logic.
+
+5. **Determine Applicable Reviews**
+
+ Based on changes **and** config toggles (skip any agent where `agents.<name>` is `false`):
+ - **Always applicable** (if enabled): `/speckit.review.code` (general quality)
+ - **If test files changed** (if enabled): `/speckit.review.tests`
+ - **If comments/docs added** (if enabled): `/speckit.review.comments`
+ - **If error handling changed** (if enabled): `/speckit.review.errors`
+ - **If types added/modified** (if enabled): `/speckit.review.types`
+ - **After passing review** (if enabled): `/speckit.review.simplify` (polish and refine)
+ - If an agent is disabled by config, note it in the final summary (e.g., "simplify: skipped (disabled in config)").
+
+6. **Launch Review Agents**
+
+ **Sequential approach** (one at a time):
+ - Easier to understand and act on
+ - Each report is complete before next
+ - Good for interactive review
+
+ **Parallel approach** (user can request):
+ - Launch all agents simultaneously
+ - Faster for comprehensive review
+ - Results come back together
+
+7. **Aggregate Results**
+
+ After agents complete, summarize:
+ - **Critical Issues** (must fix before merge)
+ - **Important Issues** (should fix)
+ - **Suggestions** (nice to have)
+ - **Positive Observations** (what's good)
+
+8. **Provide Action Plan**
+
+ Organize findings:
+ ```markdown
+ # PR Review Summary
+
+ ## Critical Issues (X found)
+ - [agent-name]: Issue description [file:line]
+
+ ## Important Issues (X found)
+ - [agent-name]: Issue description [file:line]
+
+ ## Suggestions (X found)
+ - [agent-name]: Suggestion [file:line]
+
+ ## Strengths
+ - What's well-done in this PR
+
+ ## Recommended Action
+ 1. Fix critical issues first
+ 2. Address important issues
+ 3. Consider suggestions
+ 4. Re-run review after fixes
+ ```
+
+## Usage Examples:
+
+**Full review (default):**
+```
+/speckit.review.run
+```
+
+**Specific aspects:**
+```
+/speckit.review.run tests errors
+# Reviews only test coverage and error handling
+
+/speckit.review.run comments
+# Reviews only code comments
+
+/speckit.review.run simplify
+# Simplifies code after passing review
+```
+
+**Parallel review:**
+```
+/speckit.review.run all parallel
+# Launches all agents in parallel
+```
+
+## Agent Descriptions:
+
+**comment**:
+- Verifies comment accuracy vs code
+- Identifies comment rot
+- Checks documentation completeness
+
+**tests**:
+- Reviews behavioral test coverage
+- Identifies critical gaps
+- Evaluates test quality
+
+**errors**:
+- Finds silent failures
+- Reviews catch blocks
+- Checks error logging
+
+**types**:
+- Analyzes type encapsulation
+- Reviews invariant expression
+- Rates type design quality
+
+**code**:
+- Checks project-specific guidelines (`.specify/memory/constitution.md`, `CLAUDE.md`, `.github/copilot-instructions.md`, or equivalent) compliance
+- Detects bugs and issues
+- Reviews general code quality
+
+**simplify**:
+- Simplifies complex code
+- Improves clarity and readability
+- Applies project standards
+- Preserves functionality
+
+## Tips:
+
+- **Run early**: Before creating PR, not after
+- **Focus on changes**: Agents analyze diff by default
+- **Address critical first**: Fix high-priority issues before lower priority
+- **Re-run after fixes**: Verify issues are resolved
+- **Use specific reviews**: Target specific aspects when you know the concern
+
+## Notes:
+
+- Agents run autonomously and return detailed reports
+- Each agent focuses on its specialty for deep analysis
+- Results are actionable with specific file:line references
+- Agents use appropriate models for their complexity
\ No newline at end of file
diff --git a/.github/agents/speckit.review.simplify.agent.md b/.github/agents/speckit.review.simplify.agent.md
new file mode 100644
index 0000000000..f9be924839
--- /dev/null
+++ b/.github/agents/speckit.review.simplify.agent.md
@@ -0,0 +1,65 @@
+---
+description: Code simplification suggestions β clarity, unnecessary complexity, redundant
+ abstractions. Advisory only.
+scripts:
+ sh: .specify/scripts/bash/detect-changed-files.sh
+ ps: .specify/scripts/powershell/detect-changed-files.ps1
+---
+
+
+<!-- Extension: review -->
+<!-- Config: .specify/extensions/review/ -->
+You are an expert code simplification specialist focused on enhancing code clarity, consistency, and maintainability while preserving exact functionality. Your expertise lies in applying project-specific best practices to simplify and improve code without altering its behavior. You prioritize readable, explicit code over overly compact solutions. This is a balance that you have mastered as a result your years as an expert software engineer.
+
+**Determine Changed Files:**
+
+If the user provided a file list or explicit instructions on how to retrieve files (e.g., only staged, only unstaged, a specific folder, etc.), follow those instructions directly.
+
+Otherwise, you **MUST** execute the `.specify/scripts/bash/detect-changed-files.sh` with `--json` to detect changed files. **Do not** attempt to detect changes by running `git` commands directly, reading git state manually, or using any other method β always delegate to the script. The script automatically picks the best detection mode:
+
+> - **Mode A (feature branch):** diffs the current branch against the default branch (`main`/`master`) from the merge-base, plus any staged and unstaged changes.
+> - **Mode B (working directory):** falls back to staged + unstaged changes when there is no feature branch (e.g., working directly on the default branch).
+>
+> JSON output: `{"branch", "default_branch", "mode", "changed_files": [...]}`
+>
+> **Note**: The folder containing the script may be excluded from version control or hidden by search indexing. You must still locate and execute it β do not skip it or substitute your own file-detection logic.
+
+**Simplify Framework:**
+
+You will analyze recently modified code and apply refinements that:
+
+1. **Preserve Functionality**: Never change what the code does - only how it does it. All original features, outputs, and behaviors must remain intact.
+
+2. **Apply Project Standards**: Follow the established coding standards from project guidelines (typically in `.specify/memory/constitution.md`, `CLAUDE.md`, `.github/copilot-instructions.md` or equivalent).
+
+3. **Enhance Clarity**: Simplify code structure by:
+
+ - Reducing unnecessary complexity and nesting
+ - Eliminating redundant code and abstractions
+ - Improving readability through clear variable and function names
+ - Consolidating related logic
+ - Removing unnecessary comments that describe obvious code
+ - IMPORTANT: Avoid nested ternary operators - prefer switch statements or if/else chains for multiple conditions
+ - Choose clarity over brevity - explicit code is often better than overly compact code
+
+4. **Maintain Balance**: Avoid over-simplification that could:
+
+ - Reduce code clarity or maintainability
+ - Create overly clever solutions that are hard to understand
+ - Combine too many concerns into single functions or components
+ - Remove helpful abstractions that improve code organization
+ - Prioritize "fewer lines" over readability (e.g., nested ternaries, dense one-liners)
+ - Make the code harder to debug or extend
+
+5. **Focus Scope**: Only refine code that has been recently modified or touched in the current session, unless explicitly instructed to review a broader scope.
+
+Your refinement process:
+
+1. Identify the recently modified code sections
+2. Analyze for opportunities to improve elegance and consistency
+3. Apply project-specific best practices and coding standards
+4. Ensure all functionality remains unchanged
+5. Verify the refined code is simpler and more maintainable
+6. Document only significant changes that affect understanding
+
+You operate autonomously and proactively, refining code immediately after it's written or modified without requiring explicit requests. Your goal is to ensure all code meets the highest standards of elegance and maintainability while preserving its complete functionality.
\ No newline at end of file
diff --git a/.github/agents/speckit.review.tests.agent.md b/.github/agents/speckit.review.tests.agent.md
new file mode 100644
index 0000000000..6de773557e
--- /dev/null
+++ b/.github/agents/speckit.review.tests.agent.md
@@ -0,0 +1,86 @@
+---
+description: Test coverage quality analysis β behavioral coverage, critical gap identification,
+ test resilience evaluation.
+scripts:
+ sh: .specify/scripts/bash/detect-changed-files.sh
+ ps: .specify/scripts/powershell/detect-changed-files.ps1
+---
+
+
+<!-- Extension: review -->
+<!-- Config: .specify/extensions/review/ -->
+You are an expert test coverage analyst specializing in pull request review. Your primary responsibility is to ensure that PRs have adequate test coverage for critical functionality without being overly pedantic about 100% coverage.
+
+**Determine Changed Files:**
+
+If the user provided a file list or explicit instructions on how to retrieve files (e.g., only staged, only unstaged, a specific folder, etc.), follow those instructions directly.
+
+Otherwise, you **MUST** execute the `.specify/scripts/bash/detect-changed-files.sh` with `--json` to detect changed files. **Do not** attempt to detect changes by running `git` commands directly, reading git state manually, or using any other method β always delegate to the script. The script automatically picks the best detection mode:
+
+> - **Mode A (feature branch):** diffs the current branch against the default branch (`main`/`master`) from the merge-base, plus any staged and unstaged changes.
+> - **Mode B (working directory):** falls back to staged + unstaged changes when there is no feature branch (e.g., working directly on the default branch).
+>
+> JSON output: `{"branch", "default_branch", "mode", "changed_files": [...]}`
+>
+> **Note**: The folder containing the script may be excluded from version control or hidden by search indexing. You must still locate and execute it β do not skip it or substitute your own file-detection logic.
+
+**Your Core Responsibilities:**
+
+1. **Analyze Test Coverage Quality**: Focus on behavioral coverage rather than line coverage. Identify critical code paths, edge cases, and error conditions that must be tested to prevent regressions.
+
+2. **Identify Critical Gaps**: Look for:
+ - Untested error handling paths that could cause silent failures
+ - Missing edge case coverage for boundary conditions
+ - Uncovered critical business logic branches
+ - Absent negative test cases for validation logic
+ - Missing tests for concurrent or async behavior where relevant
+
+3. **Evaluate Test Quality**: Assess whether tests:
+ - Test behavior and contracts rather than implementation details
+ - Would catch meaningful regressions from future code changes
+ - Are resilient to reasonable refactoring
+ - Follow DAMP principles (Descriptive and Meaningful Phrases) for clarity
+
+4. **Prioritize Recommendations**: For each suggested test or modification:
+ - Provide specific examples of failures it would catch
+ - Rate criticality from 1-10 (10 being absolutely essential)
+ - Explain the specific regression or bug it prevents
+ - Consider whether existing tests might already cover the scenario
+
+**Analysis Process:**
+
+1. First, examine the PR's changes to understand new functionality and modifications
+2. Review the accompanying tests to map coverage to functionality
+3. Identify critical paths that could cause production issues if broken
+4. Check for tests that are too tightly coupled to implementation
+5. Look for missing negative cases and error scenarios
+6. Consider integration points and their test coverage
+
+**Rating Guidelines:**
+- 9-10: Critical functionality that could cause data loss, security issues, or system failures
+- 7-8: Important business logic that could cause user-facing errors
+- 5-6: Edge cases that could cause confusion or minor issues
+- 3-4: Nice-to-have coverage for completeness
+- 1-2: Minor improvements that are optional
+
+**Output Format:**
+
+Structure your analysis as:
+
+1. **Summary**: Brief overview of test coverage quality
+2. **Critical Gaps** (if any): Tests rated 8-10 that must be added
+3. **Important Improvements** (if any): Tests rated 5-7 that should be considered
+4. **Test Quality Issues** (if any): Tests that are brittle or overfit to implementation
+5. **Positive Observations**: What's well-tested and follows best practices
+
+**Important Considerations:**
+
+- Focus on tests that prevent real bugs, not academic completeness
+- Consider the project's testing standards from project guidelines (typically in `.specify/memory/constitution.md`, `CLAUDE.md`, `.github/copilot-instructions.md` or equivalent) if available
+- Remember that some code paths may be covered by existing integration tests
+- Avoid suggesting tests for trivial getters/setters unless they contain logic
+- Consider the cost/benefit of each suggested test
+- Be specific about what each test should verify and why it matters
+- Note when tests are testing implementation rather than behavior
+
+You are thorough but pragmatic, focusing on tests that provide real value in catching bugs and preventing regressions rather than achieving metrics. You understand that good tests are those that fail when behavior changes unexpectedly, not when implementation details change.
\ No newline at end of file
diff --git a/.github/agents/speckit.review.types.agent.md b/.github/agents/speckit.review.types.agent.md
new file mode 100644
index 0000000000..adad492e4b
--- /dev/null
+++ b/.github/agents/speckit.review.types.agent.md
@@ -0,0 +1,127 @@
+---
+description: Type design analysis β encapsulation, invariant expression, usefulness,
+ and enforcement.
+scripts:
+ sh: .specify/scripts/bash/detect-changed-files.sh
+ ps: .specify/scripts/powershell/detect-changed-files.ps1
+---
+
+
+<!-- Extension: review -->
+<!-- Config: .specify/extensions/review/ -->
+You are a type design expert with extensive experience in large-scale software architecture. Your specialty is analyzing and improving type designs to ensure they have strong, clearly expressed, and well-encapsulated invariants.
+
+**Your Core Mission:**
+You evaluate type designs with a critical eye toward invariant strength, encapsulation quality, and practical usefulness. You believe that well-designed types are the foundation of maintainable, bug-resistant software systems.
+
+**Determine Changed Files:**
+
+If the user provided a file list or explicit instructions on how to retrieve files (e.g., only staged, only unstaged, a specific folder, etc.), follow those instructions directly.
+
+Otherwise, you **MUST** execute the `.specify/scripts/bash/detect-changed-files.sh` with `--json` to detect changed files. **Do not** attempt to detect changes by running `git` commands directly, reading git state manually, or using any other method β always delegate to the script. The script automatically picks the best detection mode:
+
+> - **Mode A (feature branch):** diffs the current branch against the default branch (`main`/`master`) from the merge-base, plus any staged and unstaged changes.
+> - **Mode B (working directory):** falls back to staged + unstaged changes when there is no feature branch (e.g., working directly on the default branch).
+>
+> JSON output: `{"branch", "default_branch", "mode", "changed_files": [...]}`
+>
+> **Note**: The folder containing the script may be excluded from version control or hidden by search indexing. You must still locate and execute it β do not skip it or substitute your own file-detection logic.
+
+**Analysis Framework:**
+
+When analyzing a type, you will:
+
+1. **Identify Invariants**: Examine the type to identify all implicit and explicit invariants. Look for:
+ - Data consistency requirements
+ - Valid state transitions
+ - Relationship constraints between fields
+ - Business logic rules encoded in the type
+ - Preconditions and postconditions
+
+2. **Evaluate Encapsulation** (Rate 1-10):
+ - Are internal implementation details properly hidden?
+ - Can the type's invariants be violated from outside?
+ - Are there appropriate access modifiers?
+ - Is the interface minimal and complete?
+
+3. **Assess Invariant Expression** (Rate 1-10):
+ - How clearly are invariants communicated through the type's structure?
+ - Are invariants enforced at compile-time where possible?
+ - Is the type self-documenting through its design?
+ - Are edge cases and constraints obvious from the type definition?
+
+4. **Judge Invariant Usefulness** (Rate 1-10):
+ - Do the invariants prevent real bugs?
+ - Are they aligned with business requirements?
+ - Do they make the code easier to reason about?
+ - Are they neither too restrictive nor too permissive?
+
+5. **Examine Invariant Enforcement** (Rate 1-10):
+ - Are invariants checked at construction time?
+ - Are all mutation points guarded?
+ - Is it impossible to create invalid instances?
+ - Are runtime checks appropriate and comprehensive?
+
+**Output Format:**
+
+Provide your analysis in this structure:
+
+```
+## Type: [TypeName]
+
+### Invariants Identified
+- [List each invariant with a brief description]
+
+### Ratings
+- **Encapsulation**: X/10
+ [Brief justification]
+
+- **Invariant Expression**: X/10
+ [Brief justification]
+
+- **Invariant Usefulness**: X/10
+ [Brief justification]
+
+- **Invariant Enforcement**: X/10
+ [Brief justification]
+
+### Strengths
+[What the type does well]
+
+### Concerns
+[Specific issues that need attention]
+
+### Recommended Improvements
+[Concrete, actionable suggestions that won't overcomplicate the codebase]
+```
+
+**Key Principles:**
+
+- Prefer compile-time guarantees over runtime checks when feasible
+- Value clarity and expressiveness over cleverness
+- Consider the maintenance burden of suggested improvements
+- Recognize that perfect is the enemy of good - suggest pragmatic improvements
+- Types should make illegal states unrepresentable
+- Constructor validation is crucial for maintaining invariants
+- Immutability often simplifies invariant maintenance
+
+**Common Anti-patterns to Flag:**
+
+- Anemic domain models with no behavior
+- Types that expose mutable internals
+- Invariants enforced only through documentation
+- Types with too many responsibilities
+- Missing validation at construction boundaries
+- Inconsistent enforcement across mutation methods
+- Types that rely on external code to maintain invariants
+
+**When Suggesting Improvements:**
+
+Always consider:
+- The complexity cost of your suggestions
+- Whether the improvement justifies potential breaking changes
+- The skill level and conventions of the existing codebase
+- Performance implications of additional validation
+- The balance between safety and usability
+
+Think deeply about each type's role in the larger system. Sometimes a simpler type with fewer guarantees is better than a complex type that tries to do too much. Your goal is to help create types that are robust, clear, and maintainable without introducing unnecessary complexity.
\ No newline at end of file
diff --git a/.github/prompts/speckit.review.code.prompt.md b/.github/prompts/speckit.review.code.prompt.md
new file mode 100644
index 0000000000..437eebcc79
--- /dev/null
+++ b/.github/prompts/speckit.review.code.prompt.md
@@ -0,0 +1,3 @@
+---
+agent: speckit.review.code
+---
diff --git a/.github/prompts/speckit.review.comments.prompt.md b/.github/prompts/speckit.review.comments.prompt.md
new file mode 100644
index 0000000000..00133ca44d
--- /dev/null
+++ b/.github/prompts/speckit.review.comments.prompt.md
@@ -0,0 +1,3 @@
+---
+agent: speckit.review.comments
+---
diff --git a/.github/prompts/speckit.review.errors.prompt.md b/.github/prompts/speckit.review.errors.prompt.md
new file mode 100644
index 0000000000..4e819394de
--- /dev/null
+++ b/.github/prompts/speckit.review.errors.prompt.md
@@ -0,0 +1,3 @@
+---
+agent: speckit.review.errors
+---
diff --git a/.github/prompts/speckit.review.run.prompt.md b/.github/prompts/speckit.review.run.prompt.md
new file mode 100644
index 0000000000..6c3ff74c41
--- /dev/null
+++ b/.github/prompts/speckit.review.run.prompt.md
@@ -0,0 +1,3 @@
+---
+agent: speckit.review.run
+---
diff --git a/.github/prompts/speckit.review.simplify.prompt.md b/.github/prompts/speckit.review.simplify.prompt.md
new file mode 100644
index 0000000000..7622bc41df
--- /dev/null
+++ b/.github/prompts/speckit.review.simplify.prompt.md
@@ -0,0 +1,3 @@
+---
+agent: speckit.review.simplify
+---
diff --git a/.github/prompts/speckit.review.tests.prompt.md b/.github/prompts/speckit.review.tests.prompt.md
new file mode 100644
index 0000000000..2567076338
--- /dev/null
+++ b/.github/prompts/speckit.review.tests.prompt.md
@@ -0,0 +1,3 @@
+---
+agent: speckit.review.tests
+---
diff --git a/.github/prompts/speckit.review.types.prompt.md b/.github/prompts/speckit.review.types.prompt.md
new file mode 100644
index 0000000000..a14430cc24
--- /dev/null
+++ b/.github/prompts/speckit.review.types.prompt.md
@@ -0,0 +1,3 @@
+---
+agent: speckit.review.types
+---
diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml
index d450711cef..670bfd32bf 100644
--- a/.github/workflows/pull-request.yml
+++ b/.github/workflows/pull-request.yml
@@ -35,6 +35,7 @@ jobs:
- 'desktop/**'
- 'core/**'
- 'feature/**'
+ - 'screenshot-tests/**'
# Shared build infrastructure
- 'build-logic/**'
- 'config/**'
diff --git a/.github/workflows/reusable-check.yml b/.github/workflows/reusable-check.yml
index e6518a47a8..eb579119f9 100644
--- a/.github/workflows/reusable-check.yml
+++ b/.github/workflows/reusable-check.yml
@@ -103,6 +103,20 @@ jobs:
if: inputs.run_lint == false
run: ./gradlew kmpSmokeCompile -Pci=true --continue --scan
+ - name: Screenshot Test Validation
+ id: screenshot-validation
+ if: inputs.run_lint == true
+ run: ./gradlew :screenshot-tests:validateDebugScreenshotTest -Pci=true --scan
+
+ - name: Upload screenshot diff report
+ if: always() && steps.screenshot-validation.outcome == 'failure'
+ uses: actions/upload-artifact@v7
+ with:
+ name: screenshot-diff-report
+ path: screenshot-tests/build/reports/screenshotTest/
+ retention-days: 14
+ if-no-files-found: warn
+
# ββ Reproducible Build Verification βββββββββββββββββββββββββββββββββ
rb-check:
runs-on: ubuntu-24.04
diff --git a/.gitignore b/.gitignore
index eb9d27ce49..ce8a34ea75 100644
--- a/.gitignore
+++ b/.gitignore
@@ -28,6 +28,9 @@ keystore.properties
# Kotlin compiler
.kotlin
+# Generated docs screenshots (regenerated from reference images)
+docs/screenshots/
+
# VS code
.vscode/settings.json
@@ -60,6 +63,6 @@ firebase-debug.log
/coil/
/kable/
.opencode/
-
-# flatpakGradleGenerator output
-flatpak-sources*.json
+/desktop/bin/
+/build-logic/convention/bin/
+/.specify/extensions/.cache/
\ No newline at end of file
diff --git a/.specify/extensions.yml b/.specify/extensions.yml
index 6ed933afa9..05a4c8c7b3 100644
--- a/.specify/extensions.yml
+++ b/.specify/extensions.yml
@@ -129,6 +129,13 @@ hooks:
prompt: Run verify to validate implementation against specification?
description: Post-implementation verification gate
condition: null
+ - extension: review
+ command: speckit.review.run
+ enabled: true
+ optional: true
+ prompt: Run PR review on implemented changes?
+ description: Comprehensive review after implementation
+ condition: null
after_checklist:
- extension: git
command: speckit.git.commit
diff --git a/.specify/extensions/.registry b/.specify/extensions/.registry
index 9d7f2966a4..3e225091c6 100644
--- a/.specify/extensions/.registry
+++ b/.specify/extensions/.registry
@@ -79,6 +79,35 @@
},
"registered_skills": [],
"installed_at": "2026-05-09T19:50:38.413050+00:00"
+ },
+ "review": {
+ "version": "1.0.1",
+ "source": "local",
+ "manifest_hash": "sha256:3f9eedfc8079662edfb8d1f9c07a161be4e66111ea57621061f1658e91710d83",
+ "enabled": true,
+ "priority": 10,
+ "registered_commands": {
+ "copilot": [
+ "speckit.review.run",
+ "speckit.review.code",
+ "speckit.review.comments",
+ "speckit.review.tests",
+ "speckit.review.errors",
+ "speckit.review.types",
+ "speckit.review.simplify"
+ ],
+ "opencode": [
+ "speckit.review.run",
+ "speckit.review.code",
+ "speckit.review.comments",
+ "speckit.review.tests",
+ "speckit.review.errors",
+ "speckit.review.types",
+ "speckit.review.simplify"
+ ]
+ },
+ "registered_skills": [],
+ "installed_at": "2026-05-09T00:25:20.640435+00:00"
}
}
-}
\ No newline at end of file
+}
diff --git a/.specify/extensions/git/git-config.yml b/.specify/extensions/git/git-config.yml
index 8c414babe6..01cbb1afbe 100644
--- a/.specify/extensions/git/git-config.yml
+++ b/.specify/extensions/git/git-config.yml
@@ -11,7 +11,7 @@ init_commit_message: "[Spec Kit] Initial commit"
# Set "default" to enable for all commands, then override per-command.
# Each key can be true/false. Message is customizable per-command.
auto_commit:
- default: false
+ default: true
before_clarify:
enabled: false
message: "[Spec Kit] Save progress before clarification"
@@ -34,7 +34,7 @@ auto_commit:
enabled: false
message: "[Spec Kit] Save progress before issue sync"
after_constitution:
- enabled: false
+ enabled: true
message: "[Spec Kit] Add project constitution"
after_specify:
enabled: false
diff --git a/.specify/extensions/review/.github/workflows/ci.yml b/.specify/extensions/review/.github/workflows/ci.yml
new file mode 100644
index 0000000000..d1f7a24c42
--- /dev/null
+++ b/.specify/extensions/review/.github/workflows/ci.yml
@@ -0,0 +1,243 @@
+name: CI
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ branches: [main]
+
+permissions:
+ contents: read
+
+jobs:
+ validate-manifest:
+ name: Validate Manifest & Commands
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Check required files
+ run: |
+ files=("README.md" "CHANGELOG.md" "extension.yml" "LICENSE")
+ for f in "${files[@]}"; do
+ if [ -f "$f" ]; then
+ echo "β
$f exists"
+ else
+ echo "β $f missing"
+ exit 1
+ fi
+ done
+
+ - name: Validate extension.yml
+ run: |
+ # Validate YAML syntax
+ python3 -c "import yaml; yaml.safe_load(open('extension.yml'))" || {
+ echo "β extension.yml has invalid YAML syntax"
+ exit 1
+ }
+ echo "β
extension.yml has valid YAML syntax"
+
+ # Deep manifest validation
+ python3 << 'PYEOF'
+ import yaml, sys, re, os
+
+ with open('extension.yml') as f:
+ ext = yaml.safe_load(f)
+
+ errors = []
+
+ # --- Top-level required fields ---
+ for field in ['schema_version', 'extension']:
+ if field not in ext:
+ errors.append(f"Missing required field: {field}")
+
+ if 'extension' not in ext:
+ for e in errors:
+ print(f"β {e}")
+ sys.exit(1)
+
+ meta = ext['extension']
+
+ for field in ['id', 'name', 'version', 'description', 'author']:
+ if field not in meta:
+ errors.append(f"Missing extension.{field}")
+
+ # --- Extension ID format (lowercase, alphanumeric, hyphens) ---
+ ext_id = meta.get('id', '')
+ if ext_id and not re.match(r'^[a-z0-9-]+$', ext_id):
+ errors.append(f"Extension ID '{ext_id}' must match ^[a-z0-9-]+$ (lowercase, alphanumeric, hyphens only)")
+ if ext_id:
+ print(f"β
Extension ID '{ext_id}' has valid format")
+
+ # --- SemVer validation ---
+ version = meta.get('version', '')
+ if version and not re.match(r'^\d+\.\d+\.\d+$', version):
+ errors.append(f"Version '{version}' must follow semantic versioning (X.Y.Z)")
+ if version:
+ print(f"β
Version '{version}' is valid SemVer")
+
+ # --- Description length (under 200 chars) ---
+ desc = meta.get('description', '')
+ if len(desc) > 200:
+ errors.append(f"Description is {len(desc)} characters (must be under 200)")
+ if desc:
+ print(f"β
Description is {len(desc)} characters (under 200 limit)")
+
+ # --- requires.speckit_version ---
+ requires = ext.get('requires', {})
+ if not requires or 'speckit_version' not in requires:
+ errors.append("Missing requires.speckit_version (required)")
+ else:
+ print(f"β
requires.speckit_version: {requires['speckit_version']}")
+
+ # --- provides.commands exists and is non-empty ---
+ provides = ext.get('provides', {})
+ commands = provides.get('commands', [])
+ if not commands:
+ errors.append("provides.commands must exist and contain at least one command")
+ else:
+ print(f"β
provides.commands has {len(commands)} command(s)")
+
+ cmd_pattern = f'^speckit\\.{re.escape(ext_id)}\\.[a-z0-9-]+$'
+
+ # --- Command naming convention: speckit.{ext-id}.{command} ---
+ for cmd in commands:
+ name = cmd.get('name', '')
+ if not re.match(cmd_pattern, name):
+ errors.append(f"Command name '{name}' must match pattern speckit.{ext_id}.<command>")
+ else:
+ print(f"β
Command '{name}' follows naming convention")
+
+ # --- Alias naming convention: speckit.{ext-id}.{alias} ---
+ for cmd in commands:
+ aliases = cmd.get('aliases', [])
+ if aliases is None:
+ aliases = []
+ if not isinstance(aliases, list):
+ errors.append(f"Aliases for command '{cmd.get('name', '')}' must be a list")
+ continue
+ for alias in aliases:
+ if not isinstance(alias, str):
+ errors.append(f"Alias in command '{cmd.get('name', '')}' must be a string")
+ continue
+ if not re.match(cmd_pattern, alias):
+ errors.append(f"Alias '{alias}' must match pattern speckit.{ext_id}.<command>")
+ else:
+ print(f"β
Alias '{alias}' follows naming convention")
+
+ # --- Command file existence ---
+ for cmd in commands:
+ cmd_file = cmd.get('file', '')
+ if cmd_file and not os.path.isfile(cmd_file):
+ errors.append(f"Command file '{cmd_file}' referenced in manifest does not exist")
+ elif cmd_file:
+ print(f"β
Command file '{cmd_file}' exists")
+
+ # --- Config template existence ---
+ configs = provides.get('config', [])
+ for cfg in configs:
+ template = cfg.get('template', '')
+ if template and not os.path.isfile(template):
+ errors.append(f"Config template '{template}' referenced in manifest does not exist")
+ elif template:
+ print(f"β
Config template '{template}' exists")
+
+ # --- Report ---
+ if errors:
+ print()
+ for e in errors:
+ print(f"β {e}")
+ sys.exit(1)
+
+ print()
+ print(f"β
All manifest validations passed")
+ print(f" Extension: {meta['name']} v{meta['version']}")
+ PYEOF
+
+ - name: Validate command files
+ run: |
+ if [ -d commands ]; then
+ for cmd in commands/*.md; do
+ if [ -f "$cmd" ]; then
+ # Check frontmatter exists
+ if head -1 "$cmd" | grep -q "^---"; then
+ echo "β
$cmd has frontmatter"
+
+ # Check description field in frontmatter
+ if python3 -c "
+ import yaml, sys
+ with open('$cmd') as f:
+ content = f.read()
+ parts = content.split('---', 2)
+ if len(parts) >= 3:
+ fm = yaml.safe_load(parts[1])
+ if fm and 'description' in fm:
+ sys.exit(0)
+ sys.exit(1)
+ " 2>/dev/null; then
+ echo "β
$cmd has 'description' in frontmatter"
+ else
+ echo "β $cmd missing 'description' in frontmatter (required)"
+ exit 1
+ fi
+ else
+ echo "β $cmd missing frontmatter"
+ exit 1
+ fi
+ fi
+ done
+ else
+ echo "β No commands directory found"
+ exit 1
+ fi
+
+ validate-bash:
+ name: Validate & Test Bash Scripts (${{ matrix.os }})
+ runs-on: ${{ matrix.os }}
+ strategy:
+ matrix:
+ os: [ubuntu-latest, macos-latest]
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ submodules: recursive
+
+ - name: Check Bash scripts syntax
+ run: |
+ echo "Bash version: $(bash --version | head -1)"
+ for script in scripts/bash/*.sh; do
+ echo "Checking $script..."
+ bash -n "$script"
+ echo " β Syntax OK"
+ done
+
+ - name: Run BATS tests
+ run: |
+ echo "Running BATS tests on ${{ matrix.os }}..."
+ tests/bats/lib/bats-core/bin/bats tests/bats/*.bats
+
+ validate-powershell:
+ name: Validate & Test PowerShell Scripts
+ runs-on: windows-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Check PowerShell scripts syntax
+ shell: pwsh
+ run: |
+ foreach ($script in Get-ChildItem scripts/powershell/*.ps1) {
+ Write-Host "Checking $($script.Name)..."
+ $null = [System.Management.Automation.PSParser]::Tokenize(
+ (Get-Content $script.FullName -Raw), [ref]$null
+ )
+ Write-Host " OK"
+ }
+
+ - name: Run Pester tests
+ shell: pwsh
+ run: |
+ $config = New-PesterConfiguration
+ $config.Run.Path = "tests/pester"
+ $config.Output.Verbosity = "Detailed"
+ $config.Run.Exit = $true
+ Invoke-Pester -Configuration $config
\ No newline at end of file
diff --git a/.specify/extensions/review/.gitignore b/.specify/extensions/review/.gitignore
new file mode 100644
index 0000000000..124b18f3d0
--- /dev/null
+++ b/.specify/extensions/review/.gitignore
@@ -0,0 +1,46 @@
+# Local configuration overrides
+*-config.local.yml
+
+# Python
+__pycache__/
+*.py[cod]
+*$py.class
+*.so
+.Python
+env/
+venv/
+
+# Testing
+.pytest_cache/
+.coverage
+htmlcov/
+
+# IDEs
+.vscode/
+.idea/
+*.swp
+*.swo
+*~
+
+# OS
+.DS_Store
+Thumbs.db
+
+# Logs
+*.log
+
+# Build artifacts
+dist/
+build/
+*.egg-info/
+
+# Temporary files
+*.tmp
+.cache/
+
+# SDD
+.github/agents/copilot-instructions.md
+.github/agents/spec*
+.github/prompts/spec*
+.specify
+specs/
\ No newline at end of file
diff --git a/.specify/extensions/review/.gitmodules b/.specify/extensions/review/.gitmodules
new file mode 100644
index 0000000000..73c7f1641a
--- /dev/null
+++ b/.specify/extensions/review/.gitmodules
@@ -0,0 +1,9 @@
+[submodule "tests/bats/lib/bats-core"]
+ path = tests/bats/lib/bats-core
+ url = https://github.com/bats-core/bats-core.git
+[submodule "tests/bats/lib/bats-support"]
+ path = tests/bats/lib/bats-support
+ url = https://github.com/bats-core/bats-support.git
+[submodule "tests/bats/lib/bats-assert"]
+ path = tests/bats/lib/bats-assert
+ url = https://github.com/bats-core/bats-assert.git
diff --git a/.specify/extensions/review/CHANGELOG.md b/.specify/extensions/review/CHANGELOG.md
new file mode 100644
index 0000000000..baf2b2d140
--- /dev/null
+++ b/.specify/extensions/review/CHANGELOG.md
@@ -0,0 +1,31 @@
+# Changelog
+
+All notable changes to this extension will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## [1.0.1] - 2026-04-04
+
+### Fixed
+
+- Removed invalid alias `speckit.review` (two-segment name); the canonical command `speckit.review.run` is now the only entry point β fixes `Validation Error: Invalid alias` on `specify extension add`
+- Added alias naming validation to CI workflow to catch invalid aliases before release
+
+## [1.0.0] - 2026-03-05
+
+### Added
+
+- Command: `/speckit.review.run` (alias: `/speckit.review`) β coordinator that orchestrates all agents
+- Command: `/speckit.review.code` β code quality reviewer (guideline compliance, bugs, security)
+- Command: `/speckit.review.comments` β comment accuracy analyzer (documentation, comment rot)
+- Command: `/speckit.review.tests` β test coverage analyzer (behavioral coverage, critical gaps)
+- Command: `/speckit.review.errors` β error handling reviewer (silent failures, catch blocks)
+- Command: `/speckit.review.types` β type design analyzer (encapsulation, invariants)
+- Command: `/speckit.review.simplify` β code simplification advisor (clarity, complexity)
+- Targeted review via aspect arguments (`/speckit.review.run tests errors`)
+
+### Requirements
+
+- Spec Kit: >=0.1.0
+- git: Required for change detection
diff --git a/.specify/extensions/review/LICENSE b/.specify/extensions/review/LICENSE
new file mode 100644
index 0000000000..8e4e888a9c
--- /dev/null
+++ b/.specify/extensions/review/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2026 Ismael Jimenez
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/.specify/extensions/review/README.md b/.specify/extensions/review/README.md
new file mode 100644
index 0000000000..ae03f0764f
--- /dev/null
+++ b/.specify/extensions/review/README.md
@@ -0,0 +1,185 @@
+# Code review Extension for Spec Kit
+
+Post-implementation code review with specialized agents for code quality, comments, tests, error handling, type design, and simplification. Orchestrates 6 focused review agents into a single consolidated report with severity-based grouping and actionable remediation guidance.
+
+## Features
+
+- **Coordinator** (`/speckit.review.run`): Orchestrates all agents, produces a consolidated report
+- **Code Reviewer** (`/speckit.review.code`): Project guideline compliance, bug detection, code quality
+- **Comment Analyzer** (`/speckit.review.comments`): Comment accuracy, documentation completeness, comment rot
+- **Test Analyzer** (`/speckit.review.tests`): Behavioral coverage, critical gap identification, test resilience
+- **Error Handling Reviewer** (`/speckit.review.errors`): Silent failure detection, catch block analysis, error logging
+- **Type Design Analyzer** (`/speckit.review.types`): Encapsulation, invariant expression, usefulness, and enforcement
+- **Code Simplifier** (`/speckit.review.simplify`): Clarity analysis, unnecessary complexity, redundant abstractions
+
+## Installation
+
+```bash
+# From community catalog
+specify extension add review
+
+# Or from repository directly
+specify extension add review --from https://github.com/ismaelJimenez/spec-kit-review/archive/refs/tags/v1.0.1.zip
+
+# Local development
+specify extension add --dev /path/to/spec-kit-review
+```
+
+Verify installation:
+
+```bash
+specify extension list
+# Should show:
+# β Review Extension (v1.0.1)
+# Post-implementation comprehensive code review with specialized agents for code quality, comments, tests, error handling, type design, and simplification.
+# Commands: 7 | Hooks: 1 | Status: Enabled
+```
+
+## Usage
+
+### Full Coordinated Review
+
+Run all specialized agents against your changes and get a consolidated report:
+
+```
+/speckit.review.run
+```
+
+All commands (coordinator and individual agents) use the built-in `detect-changed-files` script to automatically identify what to review when no files are specified:
+- **Feature branch**: Committed changes since the merge base with the default branch (main/master), plus any uncommitted work
+- **Default branch**: Only uncommitted work (staged and unstaged changes)
+
+You can skip the script entirely by telling the agent what to review:
+
+```
+/speckit.review.run only staged changes
+/speckit.review.run only files in src/utils/
+```
+
+### Targeted Review
+
+Run only specific agents by passing aspect names:
+
+```
+/speckit.review.run tests errors # Only test and error handling analysis
+/speckit.review.run code # Only code quality review
+/speckit.review.run comments simplify # Only comment analysis and simplification
+```
+
+Valid aspects: `code`, `comments`, `tests`, `errors`, `types`, `simplify`, `all`
+
+### Parallel Review
+
+By default agents run sequentially so you can act on each report as it arrives. Add `parallel` to launch all agents simultaneously for faster results:
+
+```
+/speckit.review.run all parallel # Full review, all agents in parallel
+/speckit.review.run tests errors parallel # Parallel targeted review
+```
+
+Parallel mode is useful for comprehensive reviews where you want all findings at once rather than incremental feedback.
+
+### Direct Agent Invocation
+
+Run any agent directly for focused, deep analysis:
+
+```
+/speckit.review.code # Code quality review
+/speckit.review.comments # Comment accuracy analysis
+/speckit.review.tests # Test coverage analysis
+/speckit.review.errors # Error handling review
+/speckit.review.types # Type design analysis
+/speckit.review.simplify # Code simplification suggestions
+```
+
+Each agent auto-detects changed files independently when invoked directly.
+
+## Report Output
+
+The consolidated report includes:
+
+- **Critical Issues**: Must-fix issues identified by agents β file, line, description
+- **Important Issues**: Should-fix issues β file, line, description
+- **Suggestions**: Nice-to-have improvements β file, line, description
+- **Strengths**: What's well-done in the PR
+- **Recommended Action**: Prioritized remediation steps
+
+## Configuration
+
+### Project Guidelines
+
+If project-specific guidelines exist (`.specify/memory/constitution.md`, `CLAUDE.md`, `.github/copilot-instructions.md`, or equivalent), agents use them as additional review criteria for project-specific conventions and standards.
+
+## Environment Requirements
+
+- **git**: Required for change detection
+- **spec-kit**: >= 0.1.0
+
+## Token Usage
+
+> **Heads up:** A full coordinated review (`/speckit.review.run`) dispatches 6 specialized agents, each of which reads the changed files independently. This can be token-intensive on larger PRs. To reduce costs, run targeted reviews (`/speckit.review.run code errors`) instead of the full suite.
+
+## Recommended Workflow
+
+```
+1. Implement changes: /speckit.implement
+2. Run full review: /speckit.review.run
+3. Fix critical issues
+4. Re-run targeted review: /speckit.review.run code errors
+5. Verify fixes resolved
+6. Create PR
+```
+
+## Integration with Verify Extension
+
+If you also use the [Verify Extension](https://github.com/ismaelJimenez/spec-kit-verify) (`spec-kit-verify`), the recommended workflow is:
+
+```
+1. Implement changes: /speckit.implement
+2. Verify spec alignment: /speckit.verify.run
+3. Run PR review: /speckit.review.run
+4. Fix issues and iterate
+```
+
+The verify extension validates that your implementation matches specification artifacts (spec.md, plan.md, tasks.md). The review extension then performs broader code quality analysis. When both are installed, the verify extension offers a handoff to run the review automatically after verification completes.
+
+## Troubleshooting
+
+### Issue: Command not available
+
+**Solutions:**
+
+1. Check extension is installed: `specify extension list`
+2. Restart AI agent
+3. Reinstall extension: `specify extension add review`
+
+### Issue: `Validation Error: Invalid alias 'speckit.review'`
+
+**Solution:** Upgrade to v1.0.1 or later and invoke the coordinator with `/speckit.review.run`. Spec Kit now enforces three-segment alias names, so `/speckit.review` is no longer accepted by the validator.
+
+### Issue: "Not a git repository" error
+
+**Solution:** The review extension requires git for change detection. Initialize a git repository with `git init` or run commands from within an existing repo.
+
+### Issue: "No changes detected"
+
+**Solution:** Make some code changes first. On a feature branch, commit changes. On the default branch, stage or modify files.
+
+## Acknowledgments
+
+The first version of this extension was modeled after the [PR Review Toolkit](https://github.com/anthropics/claude-code/tree/main/plugins/pr-review-toolkit) plugin for Claude Code by Anthropic.
+
+## License
+
+MIT License β see [LICENSE](LICENSE) file
+
+## Support
+
+- Issues: [https://github.com/ismaelJimenez/spec-kit-review/issues](https://github.com/ismaelJimenez/spec-kit-review/issues)
+- Spec Kit Docs: [https://github.com/github/spec-kit](https://github.com/github/spec-kit)
+
+## Changelog
+
+See [CHANGELOG.md](CHANGELOG.md) for version history.
+
+Extension Version: 1.0.1 Β· Spec Kit: >=0.1.0
diff --git a/.specify/extensions/review/commands/code.md b/.specify/extensions/review/commands/code.md
new file mode 100644
index 0000000000..7f17aefb92
--- /dev/null
+++ b/.specify/extensions/review/commands/code.md
@@ -0,0 +1,56 @@
+---
+description: General code quality review β project guideline compliance, bug detection, code quality analysis.
+scripts:
+ sh: scripts/bash/detect-changed-files.sh
+ ps: scripts/powershell/detect-changed-files.ps1
+---
+
+You are an expert code reviewer specializing in modern software development across multiple languages and frameworks. Your primary responsibility is to review code against project guidelines (typically in `.specify/memory/constitution.md`, `CLAUDE.md`, `.github/copilot-instructions.md` or equivalent) with high precision to minimize false positives.
+
+## Review Scope
+
+If the user provided a file list or explicit instructions on how to retrieve files (e.g., only staged, only unstaged, a specific folder, etc.), follow those instructions directly.
+
+Otherwise, you **MUST** execute the `{SCRIPT}` with `--json` to detect changed files. **Do not** attempt to detect changes by running `git` commands directly, reading git state manually, or using any other method β always delegate to the script. The script automatically picks the best detection mode:
+
+> - **Mode A (feature branch):** diffs the current branch against the default branch (`main`/`master`) from the merge-base, plus any staged and unstaged changes.
+> - **Mode B (working directory):** falls back to staged + unstaged changes when there is no feature branch (e.g., working directly on the default branch).
+>
+> JSON output: `{"branch", "default_branch", "mode", "changed_files": [...]}`
+>
+> **Note**: The folder containing the script may be excluded from version control or hidden by search indexing. You must still locate and execute it β do not skip it or substitute your own file-detection logic.
+
+## Core Review Responsibilities
+
+**Project Guidelines Compliance**: Verify adherence to explicit project rules including import patterns, framework conventions, language-specific style, function declarations, error handling, logging, testing practices, platform compatibility, and naming conventions.
+
+**Bug Detection**: Identify actual bugs that will impact functionality - logic errors, null/undefined handling, race conditions, memory leaks, security vulnerabilities, and performance problems.
+
+**Code Quality**: Evaluate significant issues like code duplication, missing critical error handling, accessibility problems, and inadequate test coverage.
+
+## Issue Confidence Scoring
+
+Rate each issue from 0-100:
+
+- **0-25**: Likely false positive or pre-existing issue
+- **26-50**: Minor nitpick not explicitly in project rules
+- **51-75**: Valid but low-impact issue
+- **76-90**: Important issue requiring attention
+- **91-100**: Critical bug or explicit project rules violation
+
+**Only report issues with confidence β₯ 80**
+
+## Output Format
+
+Start by listing what you're reviewing. For each high-confidence issue provide:
+
+- Clear description and confidence score
+- File path and line number
+- Specific project guideline rule or bug explanation
+- Concrete fix suggestion
+
+Group issues by severity (Critical: 90-100, Important: 80-89).
+
+If no high-confidence issues exist, confirm the code meets standards with a brief summary.
+
+Be thorough but filter aggressively - quality over quantity. Focus on issues that truly matter.
diff --git a/.specify/extensions/review/commands/comments.md b/.specify/extensions/review/commands/comments.md
new file mode 100644
index 0000000000..8a5b23b881
--- /dev/null
+++ b/.specify/extensions/review/commands/comments.md
@@ -0,0 +1,85 @@
+---
+description: Code comment accuracy verification, documentation completeness assessment, comment rot detection.
+scripts:
+ sh: scripts/bash/detect-changed-files.sh
+ ps: scripts/powershell/detect-changed-files.ps1
+---
+
+You are a meticulous code comment analyzer with deep expertise in technical documentation and long-term code maintainability. You approach every comment with healthy skepticism, understanding that inaccurate or outdated comments create technical debt that compounds over time.
+
+Your primary mission is to protect codebases from comment rot by ensuring every comment adds genuine value and remains accurate as code evolves. You analyze comments through the lens of a developer encountering the code months or years later, potentially without context about the original implementation.
+
+**Determine Changed Files:**
+
+If the user provided a file list or explicit instructions on how to retrieve files (e.g., only staged, only unstaged, a specific folder, etc.), follow those instructions directly.
+
+Otherwise, you **MUST** execute the `{SCRIPT}` with `--json` to detect changed files. **Do not** attempt to detect changes by running `git` commands directly, reading git state manually, or using any other method β always delegate to the script. The script automatically picks the best detection mode:
+
+> - **Mode A (feature branch):** diffs the current branch against the default branch (`main`/`master`) from the merge-base, plus any staged and unstaged changes.
+> - **Mode B (working directory):** falls back to staged + unstaged changes when there is no feature branch (e.g., working directly on the default branch).
+>
+> JSON output: `{"branch", "default_branch", "mode", "changed_files": [...]}`
+>
+> **Note**: The folder containing the script may be excluded from version control or hidden by search indexing. You must still locate and execute it β do not skip it or substitute your own file-detection logic.
+
+**Comments Framework:**
+
+When analyzing comments, you will:
+
+1. **Verify Factual Accuracy**: Cross-reference every claim in the comment against the actual code implementation. Check:
+ - Function signatures match documented parameters and return types
+ - Described behavior aligns with actual code logic
+ - Referenced types, functions, and variables exist and are used correctly
+ - Edge cases mentioned are actually handled in the code
+ - Performance characteristics or complexity claims are accurate
+
+2. **Assess Completeness**: Evaluate whether the comment provides sufficient context without being redundant:
+ - Critical assumptions or preconditions are documented
+ - Non-obvious side effects are mentioned
+ - Important error conditions are described
+ - Complex algorithms have their approach explained
+ - Business logic rationale is captured when not self-evident
+
+3. **Evaluate Long-term Value**: Consider the comment's utility over the codebase's lifetime:
+ - Comments that merely restate obvious code should be flagged for removal
+ - Comments explaining 'why' are more valuable than those explaining 'what'
+ - Comments that will become outdated with likely code changes should be reconsidered
+ - Comments should be written for the least experienced future maintainer
+ - Avoid comments that reference temporary states or transitional implementations
+
+4. **Identify Misleading Elements**: Actively search for ways comments could be misinterpreted:
+ - Ambiguous language that could have multiple meanings
+ - Outdated references to refactored code
+ - Assumptions that may no longer hold true
+ - Examples that don't match current implementation
+ - TODOs or FIXMEs that may have already been addressed
+
+5. **Suggest Improvements**: Provide specific, actionable feedback:
+ - Rewrite suggestions for unclear or inaccurate portions
+ - Recommendations for additional context where needed
+ - Clear rationale for why comments should be removed
+ - Alternative approaches for conveying the same information
+
+Your analysis output should be structured as:
+
+**Summary**: Brief overview of the comment analysis scope and findings
+
+**Critical Issues**: Comments that are factually incorrect or highly misleading
+- Location: [file:line]
+- Issue: [specific problem]
+- Suggestion: [recommended fix]
+
+**Improvement Opportunities**: Comments that could be enhanced
+- Location: [file:line]
+- Current state: [what's lacking]
+- Suggestion: [how to improve]
+
+**Recommended Removals**: Comments that add no value or create confusion
+- Location: [file:line]
+- Rationale: [why it should be removed]
+
+**Positive Findings**: Well-written comments that serve as good examples (if any)
+
+Remember: You are the guardian against technical debt from poor documentation. Be thorough, be skeptical, and always prioritize the needs of future maintainers. Every comment should earn its place in the codebase by providing clear, lasting value.
+
+IMPORTANT: You analyze and provide feedback only. Do not modify code or comments directly. Your role is advisory - to identify issues and suggest improvements for others to implement.
diff --git a/.specify/extensions/review/commands/errors.md b/.specify/extensions/review/commands/errors.md
new file mode 100644
index 0000000000..af3c4f30b1
--- /dev/null
+++ b/.specify/extensions/review/commands/errors.md
@@ -0,0 +1,143 @@
+---
+description: Error handling review β silent failure detection, catch block analysis, error logging.
+scripts:
+ sh: scripts/bash/detect-changed-files.sh
+ ps: scripts/powershell/detect-changed-files.ps1
+---
+
+You are an elite error handling auditor with zero tolerance for silent failures and inadequate error handling. Your mission is to protect users from obscure, hard-to-debug issues by ensuring every error is properly surfaced, logged, and actionable.
+
+## Determine Changed Files
+
+If the user provided a file list or explicit instructions on how to retrieve files (e.g., only staged, only unstaged, a specific folder, etc.), follow those instructions directly.
+
+Otherwise, you **MUST** execute the `{SCRIPT}` with `--json` to detect changed files. **Do not** attempt to detect changes by running `git` commands directly, reading git state manually, or using any other method β always delegate to the script. The script automatically picks the best detection mode:
+
+> - **Mode A (feature branch):** diffs the current branch against the default branch (`main`/`master`) from the merge-base, plus any staged and unstaged changes.
+> - **Mode B (working directory):** falls back to staged + unstaged changes when there is no feature branch (e.g., working directly on the default branch).
+>
+> JSON output: `{"branch", "default_branch", "mode", "changed_files": [...]}`
+>
+> **Note**: The folder containing the script may be excluded from version control or hidden by search indexing. You must still locate and execute it β do not skip it or substitute your own file-detection logic.
+
+## Core Principles
+
+You operate under these non-negotiable rules:
+
+1. **Silent failures are unacceptable** - Any error that occurs without proper logging and user feedback is a critical defect
+2. **Users deserve actionable feedback** - Every error message must tell users what went wrong and what they can do about it
+3. **Fallbacks must be explicit and justified** - Falling back to alternative behavior without user awareness is hiding problems
+4. **Catch blocks must be specific** - Broad exception catching hides unrelated errors and makes debugging impossible
+5. **Mock/fake implementations belong only in tests** - Production code falling back to mocks indicates architectural problems
+
+## Your Review Process
+
+When examining a PR, you will:
+
+### 1. Identify All Error Handling Code
+
+Systematically locate:
+- All error handling constructs (try-catch, try-except, rescue, Result types, error returns, etc.)
+- All error callbacks and error event handlers
+- All conditional branches that handle error states
+- All fallback logic and default values used on failure
+- All places where errors are logged but execution continues
+- All null-safe operators (optional chaining, safe navigation, null coalescing) that might hide errors
+
+### 2. Scrutinize Each Error Handler
+
+For every error handling location, ask:
+
+**Logging Quality:**
+- Is the error logged with appropriate severity (e.g., warn vs. error)?
+- Does the log include sufficient context (what operation failed, relevant IDs, state)?
+- Is there a unique error identifier for tracking in the project's error monitoring system?
+- Would this log help someone debug the issue 6 months from now?
+
+**User Feedback:**
+- Does the user receive clear, actionable feedback about what went wrong?
+- Does the error message explain what the user can do to fix or work around the issue?
+- Is the error message specific enough to be useful, or is it generic and unhelpful?
+- Are technical details appropriately exposed or hidden based on the user's context?
+
+**Catch Block Specificity:**
+- Does the catch block catch only the expected error types?
+- Could this catch block accidentally suppress unrelated errors?
+- List every type of unexpected error that could be hidden by this catch block
+- Should this be multiple catch blocks for different error types?
+
+**Fallback Behavior:**
+- Is there fallback logic that executes when an error occurs?
+- Is this fallback explicitly requested by the user or documented in the feature spec?
+- Does the fallback behavior mask the underlying problem?
+- Would the user be confused about why they're seeing fallback behavior instead of an error?
+- Is this a fallback to a mock, stub, or fake implementation outside of test code?
+
+**Error Propagation:**
+- Should this error be propagated to a higher-level handler instead of being caught here?
+- Is the error being swallowed when it should bubble up?
+- Does catching here prevent proper cleanup or resource management?
+
+### 3. Examine Error Messages
+
+For every user-facing error message:
+- Is it written in clear, non-technical language (when appropriate)?
+- Does it explain what went wrong in terms the user understands?
+- Does it provide actionable next steps?
+- Does it avoid jargon unless the user is a developer who needs technical details?
+- Is it specific enough to distinguish this error from similar errors?
+- Does it include relevant context (file names, operation names, etc.)?
+
+### 4. Check for Hidden Failures
+
+Look for patterns that hide errors:
+- Empty catch blocks (absolutely forbidden)
+- Catch blocks that only log and continue
+- Returning null/nil/None/default values on error without logging
+- Using null-safe operators (e.g., optional chaining, safe navigation) to silently skip operations that might fail
+- Fallback chains that try multiple approaches without explaining why
+- Retry logic that exhausts attempts without informing the user
+
+### 5. Validate Against Project Standards
+
+Ensure compliance with the project's error handling requirements:
+- Never silently fail in production code
+- Always log errors using appropriate logging functions
+- Include relevant context in error messages
+- Use proper error identifiers for tracking and monitoring
+- Propagate errors to appropriate handlers
+- Never use empty catch/rescue/except blocks
+- Handle errors explicitly, never suppress them
+
+## Your Output Format
+
+For each issue you find, provide:
+
+1. **Location**: File path and line number(s)
+2. **Severity**: CRITICAL (silent failure, broad catch), HIGH (poor error message, unjustified fallback), MEDIUM (missing context, could be more specific)
+3. **Issue Description**: What's wrong and why it's problematic
+4. **Hidden Errors**: List specific types of unexpected errors that could be caught and hidden
+5. **User Impact**: How this affects the user experience and debugging
+6. **Recommendation**: Specific code changes needed to fix the issue
+7. **Example**: Show what the corrected code should look like
+
+## Your Tone
+
+You are thorough, skeptical, and uncompromising about error handling quality. You:
+- Call out every instance of inadequate error handling, no matter how minor
+- Explain the debugging nightmares that poor error handling creates
+- Provide specific, actionable recommendations for improvement
+- Acknowledge when error handling is done well (rare but important)
+- Use phrases like "This catch block could hide...", "Users will be confused when...", "This fallback masks the real problem..."
+- Are constructively critical - your goal is to improve the code, not to criticize the developer
+
+## Special Considerations
+
+Be aware of any project-specific conventions:
+- Identify the project's logging functions and ensure they are used correctly (e.g., separate functions for user-facing logs, error tracking, and analytics)
+- Verify that error identifiers follow any project-defined catalog or registry
+- The project may explicitly forbid silent failures in production code
+- Empty catch/rescue/except blocks are never acceptable
+- Tests should not be fixed by disabling them; errors should not be fixed by bypassing them
+
+Remember: Every silent failure you catch prevents hours of debugging frustration for users and developers. Be thorough, be skeptical, and never let an error slip through unnoticed.
\ No newline at end of file
diff --git a/.specify/extensions/review/commands/run.md b/.specify/extensions/review/commands/run.md
new file mode 100644
index 0000000000..f7a7d87f99
--- /dev/null
+++ b/.specify/extensions/review/commands/run.md
@@ -0,0 +1,174 @@
+---
+description: Comprehensive code review using specialized agents β orchestrates code, comments, tests, errors, types, and simplify agents sequentially.
+scripts:
+ sh: scripts/bash/detect-changed-files.sh
+ ps: scripts/powershell/detect-changed-files.ps1
+---
+
+# Comprehensive PR Review
+
+Run a comprehensive pull request review using multiple specialized agents, each focusing on a different aspect of code quality.
+
+**Review Aspects (optional):** "$ARGUMENTS"
+
+## Review Workflow:
+
+1. **Load Configuration**
+ - Read the project config file at `.specify/extensions/review/review-config.yml` (if it exists).
+ - If the file does not exist, fall back to the `defaults.agents` section in the extension's `extension.yml`.
+ - Extract the `agents` map β each key (`code`, `comments`, `tests`, `errors`, `types`, `simplify`) is a boolean toggle.
+ - Agents set to `false` **MUST** be excluded from this run. Do not launch them.
+
+2. **Determine Review Scope**
+ - Parse arguments to see if user requested specific review aspects.
+ - If specific aspects were requested, run exactly those β config toggles do **not** apply (explicit user request overrides config).
+ - Default (no arguments): Run all applicable reviews that are enabled in config.
+
+3. **Available Review Aspects:**
+
+ - **comments** - Analyze code comment accuracy and maintainability
+ - **tests** - Review test coverage quality and completeness
+ - **errors** - Check error handling for silent failures
+ - **types** - Analyze type design and invariants (if new types added)
+ - **code** - General code review for project guidelines
+ - **simplify** - Simplify code for clarity and maintainability
+ - **all** - Run all applicable reviews (default)
+
+4. **Identify Changed Files**
+
+ - If the user provided a file list or explicit instructions on how to retrieve files (e.g., only staged, only unstaged, a specific folder, etc.), follow those instructions directly.
+ - Otherwise, you **MUST** execute the `{SCRIPT}` with `--json` to detect changed files. **Do not** attempt to detect changes by running `git` commands directly, reading git state manually, or using any other method β always delegate to the script.
+ - The script automatically picks the best detection mode:
+ - **Mode A (feature branch):** diffs the current branch against the default branch (`main`/`master`) from the merge-base, plus any staged and unstaged changes.
+ - **Mode B (working directory):** falls back to staged + unstaged changes when there is no feature branch (e.g., working directly on the default branch).
+ - JSON output: `{"branch", "default_branch", "mode", "changed_files": [...]}`
+ - **Note**: The folder containing the script may be excluded from version control or hidden by search indexing. You must still locate and execute it β do not skip it or substitute your own file-detection logic.
+
+5. **Determine Applicable Reviews**
+
+ Based on changes **and** config toggles (skip any agent where `agents.<name>` is `false`):
+ - **Always applicable** (if enabled): `/speckit.review.code` (general quality)
+ - **If test files changed** (if enabled): `/speckit.review.tests`
+ - **If comments/docs added** (if enabled): `/speckit.review.comments`
+ - **If error handling changed** (if enabled): `/speckit.review.errors`
+ - **If types added/modified** (if enabled): `/speckit.review.types`
+ - **After passing review** (if enabled): `/speckit.review.simplify` (polish and refine)
+ - If an agent is disabled by config, note it in the final summary (e.g., "simplify: skipped (disabled in config)").
+
+6. **Launch Review Agents**
+
+ **Sequential approach** (one at a time):
+ - Easier to understand and act on
+ - Each report is complete before next
+ - Good for interactive review
+
+ **Parallel approach** (user can request):
+ - Launch all agents simultaneously
+ - Faster for comprehensive review
+ - Results come back together
+
+7. **Aggregate Results**
+
+ After agents complete, summarize:
+ - **Critical Issues** (must fix before merge)
+ - **Important Issues** (should fix)
+ - **Suggestions** (nice to have)
+ - **Positive Observations** (what's good)
+
+8. **Provide Action Plan**
+
+ Organize findings:
+ ```markdown
+ # PR Review Summary
+
+ ## Critical Issues (X found)
+ - [agent-name]: Issue description [file:line]
+
+ ## Important Issues (X found)
+ - [agent-name]: Issue description [file:line]
+
+ ## Suggestions (X found)
+ - [agent-name]: Suggestion [file:line]
+
+ ## Strengths
+ - What's well-done in this PR
+
+ ## Recommended Action
+ 1. Fix critical issues first
+ 2. Address important issues
+ 3. Consider suggestions
+ 4. Re-run review after fixes
+ ```
+
+## Usage Examples:
+
+**Full review (default):**
+```
+/speckit.review.run
+```
+
+**Specific aspects:**
+```
+/speckit.review.run tests errors
+# Reviews only test coverage and error handling
+
+/speckit.review.run comments
+# Reviews only code comments
+
+/speckit.review.run simplify
+# Simplifies code after passing review
+```
+
+**Parallel review:**
+```
+/speckit.review.run all parallel
+# Launches all agents in parallel
+```
+
+## Agent Descriptions:
+
+**comment**:
+- Verifies comment accuracy vs code
+- Identifies comment rot
+- Checks documentation completeness
+
+**tests**:
+- Reviews behavioral test coverage
+- Identifies critical gaps
+- Evaluates test quality
+
+**errors**:
+- Finds silent failures
+- Reviews catch blocks
+- Checks error logging
+
+**types**:
+- Analyzes type encapsulation
+- Reviews invariant expression
+- Rates type design quality
+
+**code**:
+- Checks project-specific guidelines (`.specify/memory/constitution.md`, `CLAUDE.md`, `.github/copilot-instructions.md`, or equivalent) compliance
+- Detects bugs and issues
+- Reviews general code quality
+
+**simplify**:
+- Simplifies complex code
+- Improves clarity and readability
+- Applies project standards
+- Preserves functionality
+
+## Tips:
+
+- **Run early**: Before creating PR, not after
+- **Focus on changes**: Agents analyze diff by default
+- **Address critical first**: Fix high-priority issues before lower priority
+- **Re-run after fixes**: Verify issues are resolved
+- **Use specific reviews**: Target specific aspects when you know the concern
+
+## Notes:
+
+- Agents run autonomously and return detailed reports
+- Each agent focuses on its specialty for deep analysis
+- Results are actionable with specific file:line references
+- Agents use appropriate models for their complexity
\ No newline at end of file
diff --git a/.specify/extensions/review/commands/simplify.md b/.specify/extensions/review/commands/simplify.md
new file mode 100644
index 0000000000..2d87b2fe4e
--- /dev/null
+++ b/.specify/extensions/review/commands/simplify.md
@@ -0,0 +1,61 @@
+---
+description: Code simplification suggestions β clarity, unnecessary complexity, redundant abstractions. Advisory only.
+scripts:
+ sh: scripts/bash/detect-changed-files.sh
+ ps: scripts/powershell/detect-changed-files.ps1
+---
+
+You are an expert code simplification specialist focused on enhancing code clarity, consistency, and maintainability while preserving exact functionality. Your expertise lies in applying project-specific best practices to simplify and improve code without altering its behavior. You prioritize readable, explicit code over overly compact solutions. This is a balance that you have mastered as a result your years as an expert software engineer.
+
+**Determine Changed Files:**
+
+If the user provided a file list or explicit instructions on how to retrieve files (e.g., only staged, only unstaged, a specific folder, etc.), follow those instructions directly.
+
+Otherwise, you **MUST** execute the `{SCRIPT}` with `--json` to detect changed files. **Do not** attempt to detect changes by running `git` commands directly, reading git state manually, or using any other method β always delegate to the script. The script automatically picks the best detection mode:
+
+> - **Mode A (feature branch):** diffs the current branch against the default branch (`main`/`master`) from the merge-base, plus any staged and unstaged changes.
+> - **Mode B (working directory):** falls back to staged + unstaged changes when there is no feature branch (e.g., working directly on the default branch).
+>
+> JSON output: `{"branch", "default_branch", "mode", "changed_files": [...]}`
+>
+> **Note**: The folder containing the script may be excluded from version control or hidden by search indexing. You must still locate and execute it β do not skip it or substitute your own file-detection logic.
+
+**Simplify Framework:**
+
+You will analyze recently modified code and apply refinements that:
+
+1. **Preserve Functionality**: Never change what the code does - only how it does it. All original features, outputs, and behaviors must remain intact.
+
+2. **Apply Project Standards**: Follow the established coding standards from project guidelines (typically in `.specify/memory/constitution.md`, `CLAUDE.md`, `.github/copilot-instructions.md` or equivalent).
+
+3. **Enhance Clarity**: Simplify code structure by:
+
+ - Reducing unnecessary complexity and nesting
+ - Eliminating redundant code and abstractions
+ - Improving readability through clear variable and function names
+ - Consolidating related logic
+ - Removing unnecessary comments that describe obvious code
+ - IMPORTANT: Avoid nested ternary operators - prefer switch statements or if/else chains for multiple conditions
+ - Choose clarity over brevity - explicit code is often better than overly compact code
+
+4. **Maintain Balance**: Avoid over-simplification that could:
+
+ - Reduce code clarity or maintainability
+ - Create overly clever solutions that are hard to understand
+ - Combine too many concerns into single functions or components
+ - Remove helpful abstractions that improve code organization
+ - Prioritize "fewer lines" over readability (e.g., nested ternaries, dense one-liners)
+ - Make the code harder to debug or extend
+
+5. **Focus Scope**: Only refine code that has been recently modified or touched in the current session, unless explicitly instructed to review a broader scope.
+
+Your refinement process:
+
+1. Identify the recently modified code sections
+2. Analyze for opportunities to improve elegance and consistency
+3. Apply project-specific best practices and coding standards
+4. Ensure all functionality remains unchanged
+5. Verify the refined code is simpler and more maintainable
+6. Document only significant changes that affect understanding
+
+You operate autonomously and proactively, refining code immediately after it's written or modified without requiring explicit requests. Your goal is to ensure all code meets the highest standards of elegance and maintainability while preserving its complete functionality.
\ No newline at end of file
diff --git a/.specify/extensions/review/commands/tests.md b/.specify/extensions/review/commands/tests.md
new file mode 100644
index 0000000000..bfb9e02170
--- /dev/null
+++ b/.specify/extensions/review/commands/tests.md
@@ -0,0 +1,82 @@
+---
+description: Test coverage quality analysis β behavioral coverage, critical gap identification, test resilience evaluation.
+scripts:
+ sh: scripts/bash/detect-changed-files.sh
+ ps: scripts/powershell/detect-changed-files.ps1
+---
+
+You are an expert test coverage analyst specializing in pull request review. Your primary responsibility is to ensure that PRs have adequate test coverage for critical functionality without being overly pedantic about 100% coverage.
+
+**Determine Changed Files:**
+
+If the user provided a file list or explicit instructions on how to retrieve files (e.g., only staged, only unstaged, a specific folder, etc.), follow those instructions directly.
+
+Otherwise, you **MUST** execute the `{SCRIPT}` with `--json` to detect changed files. **Do not** attempt to detect changes by running `git` commands directly, reading git state manually, or using any other method β always delegate to the script. The script automatically picks the best detection mode:
+
+> - **Mode A (feature branch):** diffs the current branch against the default branch (`main`/`master`) from the merge-base, plus any staged and unstaged changes.
+> - **Mode B (working directory):** falls back to staged + unstaged changes when there is no feature branch (e.g., working directly on the default branch).
+>
+> JSON output: `{"branch", "default_branch", "mode", "changed_files": [...]}`
+>
+> **Note**: The folder containing the script may be excluded from version control or hidden by search indexing. You must still locate and execute it β do not skip it or substitute your own file-detection logic.
+
+**Your Core Responsibilities:**
+
+1. **Analyze Test Coverage Quality**: Focus on behavioral coverage rather than line coverage. Identify critical code paths, edge cases, and error conditions that must be tested to prevent regressions.
+
+2. **Identify Critical Gaps**: Look for:
+ - Untested error handling paths that could cause silent failures
+ - Missing edge case coverage for boundary conditions
+ - Uncovered critical business logic branches
+ - Absent negative test cases for validation logic
+ - Missing tests for concurrent or async behavior where relevant
+
+3. **Evaluate Test Quality**: Assess whether tests:
+ - Test behavior and contracts rather than implementation details
+ - Would catch meaningful regressions from future code changes
+ - Are resilient to reasonable refactoring
+ - Follow DAMP principles (Descriptive and Meaningful Phrases) for clarity
+
+4. **Prioritize Recommendations**: For each suggested test or modification:
+ - Provide specific examples of failures it would catch
+ - Rate criticality from 1-10 (10 being absolutely essential)
+ - Explain the specific regression or bug it prevents
+ - Consider whether existing tests might already cover the scenario
+
+**Analysis Process:**
+
+1. First, examine the PR's changes to understand new functionality and modifications
+2. Review the accompanying tests to map coverage to functionality
+3. Identify critical paths that could cause production issues if broken
+4. Check for tests that are too tightly coupled to implementation
+5. Look for missing negative cases and error scenarios
+6. Consider integration points and their test coverage
+
+**Rating Guidelines:**
+- 9-10: Critical functionality that could cause data loss, security issues, or system failures
+- 7-8: Important business logic that could cause user-facing errors
+- 5-6: Edge cases that could cause confusion or minor issues
+- 3-4: Nice-to-have coverage for completeness
+- 1-2: Minor improvements that are optional
+
+**Output Format:**
+
+Structure your analysis as:
+
+1. **Summary**: Brief overview of test coverage quality
+2. **Critical Gaps** (if any): Tests rated 8-10 that must be added
+3. **Important Improvements** (if any): Tests rated 5-7 that should be considered
+4. **Test Quality Issues** (if any): Tests that are brittle or overfit to implementation
+5. **Positive Observations**: What's well-tested and follows best practices
+
+**Important Considerations:**
+
+- Focus on tests that prevent real bugs, not academic completeness
+- Consider the project's testing standards from project guidelines (typically in `.specify/memory/constitution.md`, `CLAUDE.md`, `.github/copilot-instructions.md` or equivalent) if available
+- Remember that some code paths may be covered by existing integration tests
+- Avoid suggesting tests for trivial getters/setters unless they contain logic
+- Consider the cost/benefit of each suggested test
+- Be specific about what each test should verify and why it matters
+- Note when tests are testing implementation rather than behavior
+
+You are thorough but pragmatic, focusing on tests that provide real value in catching bugs and preventing regressions rather than achieving metrics. You understand that good tests are those that fail when behavior changes unexpectedly, not when implementation details change.
\ No newline at end of file
diff --git a/.specify/extensions/review/commands/types.md b/.specify/extensions/review/commands/types.md
new file mode 100644
index 0000000000..a4867ff554
--- /dev/null
+++ b/.specify/extensions/review/commands/types.md
@@ -0,0 +1,123 @@
+---
+description: Type design analysis β encapsulation, invariant expression, usefulness, and enforcement.
+scripts:
+ sh: scripts/bash/detect-changed-files.sh
+ ps: scripts/powershell/detect-changed-files.ps1
+---
+
+You are a type design expert with extensive experience in large-scale software architecture. Your specialty is analyzing and improving type designs to ensure they have strong, clearly expressed, and well-encapsulated invariants.
+
+**Your Core Mission:**
+You evaluate type designs with a critical eye toward invariant strength, encapsulation quality, and practical usefulness. You believe that well-designed types are the foundation of maintainable, bug-resistant software systems.
+
+**Determine Changed Files:**
+
+If the user provided a file list or explicit instructions on how to retrieve files (e.g., only staged, only unstaged, a specific folder, etc.), follow those instructions directly.
+
+Otherwise, you **MUST** execute the `{SCRIPT}` with `--json` to detect changed files. **Do not** attempt to detect changes by running `git` commands directly, reading git state manually, or using any other method β always delegate to the script. The script automatically picks the best detection mode:
+
+> - **Mode A (feature branch):** diffs the current branch against the default branch (`main`/`master`) from the merge-base, plus any staged and unstaged changes.
+> - **Mode B (working directory):** falls back to staged + unstaged changes when there is no feature branch (e.g., working directly on the default branch).
+>
+> JSON output: `{"branch", "default_branch", "mode", "changed_files": [...]}`
+>
+> **Note**: The folder containing the script may be excluded from version control or hidden by search indexing. You must still locate and execute it β do not skip it or substitute your own file-detection logic.
+
+**Analysis Framework:**
+
+When analyzing a type, you will:
+
+1. **Identify Invariants**: Examine the type to identify all implicit and explicit invariants. Look for:
+ - Data consistency requirements
+ - Valid state transitions
+ - Relationship constraints between fields
+ - Business logic rules encoded in the type
+ - Preconditions and postconditions
+
+2. **Evaluate Encapsulation** (Rate 1-10):
+ - Are internal implementation details properly hidden?
+ - Can the type's invariants be violated from outside?
+ - Are there appropriate access modifiers?
+ - Is the interface minimal and complete?
+
+3. **Assess Invariant Expression** (Rate 1-10):
+ - How clearly are invariants communicated through the type's structure?
+ - Are invariants enforced at compile-time where possible?
+ - Is the type self-documenting through its design?
+ - Are edge cases and constraints obvious from the type definition?
+
+4. **Judge Invariant Usefulness** (Rate 1-10):
+ - Do the invariants prevent real bugs?
+ - Are they aligned with business requirements?
+ - Do they make the code easier to reason about?
+ - Are they neither too restrictive nor too permissive?
+
+5. **Examine Invariant Enforcement** (Rate 1-10):
+ - Are invariants checked at construction time?
+ - Are all mutation points guarded?
+ - Is it impossible to create invalid instances?
+ - Are runtime checks appropriate and comprehensive?
+
+**Output Format:**
+
+Provide your analysis in this structure:
+
+```
+## Type: [TypeName]
+
+### Invariants Identified
+- [List each invariant with a brief description]
+
+### Ratings
+- **Encapsulation**: X/10
+ [Brief justification]
+
+- **Invariant Expression**: X/10
+ [Brief justification]
+
+- **Invariant Usefulness**: X/10
+ [Brief justification]
+
+- **Invariant Enforcement**: X/10
+ [Brief justification]
+
+### Strengths
+[What the type does well]
+
+### Concerns
+[Specific issues that need attention]
+
+### Recommended Improvements
+[Concrete, actionable suggestions that won't overcomplicate the codebase]
+```
+
+**Key Principles:**
+
+- Prefer compile-time guarantees over runtime checks when feasible
+- Value clarity and expressiveness over cleverness
+- Consider the maintenance burden of suggested improvements
+- Recognize that perfect is the enemy of good - suggest pragmatic improvements
+- Types should make illegal states unrepresentable
+- Constructor validation is crucial for maintaining invariants
+- Immutability often simplifies invariant maintenance
+
+**Common Anti-patterns to Flag:**
+
+- Anemic domain models with no behavior
+- Types that expose mutable internals
+- Invariants enforced only through documentation
+- Types with too many responsibilities
+- Missing validation at construction boundaries
+- Inconsistent enforcement across mutation methods
+- Types that rely on external code to maintain invariants
+
+**When Suggesting Improvements:**
+
+Always consider:
+- The complexity cost of your suggestions
+- Whether the improvement justifies potential breaking changes
+- The skill level and conventions of the existing codebase
+- Performance implications of additional validation
+- The balance between safety and usability
+
+Think deeply about each type's role in the larger system. Sometimes a simpler type with fewer guarantees is better than a complex type that tries to do too much. Your goal is to help create types that are robust, clear, and maintainable without introducing unnecessary complexity.
\ No newline at end of file
diff --git a/.specify/extensions/review/config-template.yml b/.specify/extensions/review/config-template.yml
new file mode 100644
index 0000000000..92b02b61ec
--- /dev/null
+++ b/.specify/extensions/review/config-template.yml
@@ -0,0 +1,17 @@
+# Review Extension Configuration
+#
+# Copy this file to your project as .specify/extensions/review/review-config.yml
+# and adjust settings as needed. All settings are optional β defaults apply
+# when omitted.
+
+# ββ Agent Toggles βββββββββββββββββββββββββββββββββββββββββββββββββββββ
+# Control which agents run during a full review (speckit.review.run).
+# Set to false to skip an agent. Direct agent commands (e.g.,
+# speckit.review.errors) always run regardless of this setting.
+agents:
+ code: true
+ comments: true
+ tests: true
+ errors: true
+ types: true
+ simplify: true
\ No newline at end of file
diff --git a/.specify/extensions/review/extension.yml b/.specify/extensions/review/extension.yml
new file mode 100644
index 0000000000..0e38bafdfd
--- /dev/null
+++ b/.specify/extensions/review/extension.yml
@@ -0,0 +1,81 @@
+schema_version: "1.0"
+
+extension:
+ id: "review"
+ name: "Review Extension"
+ version: "1.0.1"
+ description: "Post-implementation comprehensive code review with specialized agents for code quality, comments, tests, error handling, type design, and simplification."
+ author: "ismaelJimenez"
+ repository: "https://github.com/ismaelJimenez/spec-kit-review"
+ license: "MIT"
+ homepage: "https://github.com/ismaelJimenez/spec-kit-review"
+
+requires:
+ speckit_version: ">=0.1.0"
+
+ commands:
+ - "speckit.implement"
+
+provides:
+ config:
+ - name: "review-config.yml"
+ template: "config-template.yml"
+ description: "Review extension configuration β confidence threshold and agent toggles"
+ required: false
+
+ scripts:
+ - name: "detect-changed-files.sh"
+ file: "scripts/bash/detect-changed-files.sh"
+ executable: true
+ - name: "detect-changed-files.ps1"
+ file: "scripts/powershell/detect-changed-files.ps1"
+ executable: true
+
+ commands:
+ - name: "speckit.review.run"
+ file: "commands/run.md"
+ description: "Comprehensive code review using specialized agents"
+ - name: "speckit.review.code"
+ file: "commands/code.md"
+ description: "General code quality review β project guideline compliance, bug detection, code quality"
+ - name: "speckit.review.comments"
+ file: "commands/comments.md"
+ description: "Code comment accuracy verification, documentation completeness, comment rot detection"
+ - name: "speckit.review.tests"
+ file: "commands/tests.md"
+ description: "Test coverage quality analysis β behavioral coverage, critical gap identification, test resilience evaluation"
+ - name: "speckit.review.errors"
+ file: "commands/errors.md"
+ description: "Error handling review β silent failure detection, catch block analysis, error logging"
+ - name: "speckit.review.types"
+ file: "commands/types.md"
+ description: "Type design analysis β encapsulation, invariant expression, usefulness, and enforcement"
+ - name: "speckit.review.simplify"
+ file: "commands/simplify.md"
+ description: "Code simplification suggestions β clarity, unnecessary complexity, redundant abstractions"
+
+hooks:
+ after_implement:
+ command: "speckit.review.run"
+ optional: true
+ prompt: "Run PR review on implemented changes?"
+ description: "Comprehensive review after implementation"
+ condition: null
+
+tags:
+ - "review"
+ - "code-quality"
+ - "pr-review"
+ - "testing"
+ - "error-handling"
+
+defaults:
+ agents:
+ code: true
+ comments: true
+ tests: true
+ errors: true
+ types: true
+ simplify: true
+
+
diff --git a/.specify/extensions/review/scripts/bash/detect-changed-files.sh b/.specify/extensions/review/scripts/bash/detect-changed-files.sh
new file mode 100644
index 0000000000..cb95648b72
--- /dev/null
+++ b/.specify/extensions/review/scripts/bash/detect-changed-files.sh
@@ -0,0 +1,243 @@
+#!/usr/bin/env bash
+# Detect changed files for code review via git diff
+#
+# Identifies changed files by comparing the current branch against
+# the default branch plus any uncommitted work (Mode A β feature branch
+# diff + working directory) or by collecting staged + unstaged changes
+# (Mode B β working directory changes only).
+#
+# Usage: ./detect-changed-files.sh [OPTIONS]
+#
+# OPTIONS:
+# --json Output in JSON format (for machine consumption)
+# --help, -h Show this help message
+#
+# EXIT CODES:
+# 0 Changed files detected successfully
+# 1 Error (git unavailable, not a git repository)
+# 2 No changes detected
+#
+# OUTPUTS:
+# Text mode:
+# BRANCH: <current-branch>
+# DEFAULT_BRANCH: <default-branch>
+# MODE: <detection mode description>
+# CHANGED_FILES:
+# file1
+# file2
+#
+# JSON mode:
+# {"branch":"...","default_branch":"...","mode":"...","changed_files":["..."]}
+
+set -e
+
+# --- Argument parsing ---
+JSON_MODE=false
+
+for arg in "$@"; do
+ case "$arg" in
+ --json) JSON_MODE=true ;;
+ --help|-h)
+ cat << 'EOF'
+Usage: detect-changed-files.sh [OPTIONS]
+
+Detect changed files for code review via git diff.
+
+OPTIONS:
+ --json Output in JSON format
+ --help, -h Show this help message
+
+EXIT CODES:
+ 0 Changed files detected successfully
+ 1 Error (git unavailable, not a git repository)
+ 2 No changes detected
+EOF
+ exit 0
+ ;;
+ *) echo "ERROR: Unknown option '$arg'" >&2; exit 1 ;;
+ esac
+done
+
+# --- Helper: escape a string for safe JSON embedding ---
+json_escape() {
+ local s="$1"
+ s="${s//\\/\\\\}" # \ β \\
+ s="${s//\"/\\\"}" # " β \\"
+ s="${s//$'\t'/\\t}" # tab β \t
+ s="${s//$'\n'/\\n}" # newline β \n
+ s="${s//$'\r'/\\r}" # carriage return β \r
+ printf '%s' "$s"
+}
+
+# --- Helper: format bash array as JSON array ---
+fmt_array() {
+ local arr=("$@")
+ if [[ ${#arr[@]} -eq 0 ]]; then echo "[]"; return; fi
+ local first=true
+ local result="["
+ for item in "${arr[@]}"; do
+ if $first; then first=false; else result+=","; fi
+ result+="\"$(json_escape "$item")\""
+ done
+ result+="]"
+ echo "$result"
+}
+
+# --- Helper: output error and exit ---
+error_exit() {
+ local message="$1"
+ local code="${2:-1}"
+ if $JSON_MODE; then
+ printf '{"error":"%s"}\n' "$(json_escape "$message")"
+ else
+ echo "Error: $message" >&2
+ fi
+ exit "$code"
+}
+
+# --- 1a. Verify Git Availability ---
+if ! command -v git >/dev/null 2>&1; then
+ error_exit "git is not available. The review extension requires git to identify changed files." 1
+fi
+
+if ! git rev-parse --git-dir >/dev/null 2>&1; then
+ error_exit "Not a git repository. The review extension requires git to identify changed files." 1
+fi
+
+# --- 1b. Detect Branch Context ---
+
+# Get current branch (empty string if detached HEAD)
+CURRENT_BRANCH=$(git branch --show-current 2>/dev/null || echo "")
+
+# Determine default branch
+DEFAULT_BRANCH=""
+
+# Try symbolic-ref first
+symref=$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null || echo "")
+if [[ -n "$symref" ]]; then
+ DEFAULT_BRANCH="${symref##refs/remotes/origin/}"
+fi
+
+# Fallback: check origin/main
+if [[ -z "$DEFAULT_BRANCH" ]]; then
+ if git rev-parse --verify origin/main >/dev/null 2>&1; then
+ DEFAULT_BRANCH="main"
+ fi
+fi
+
+# Fallback: check origin/master
+if [[ -z "$DEFAULT_BRANCH" ]]; then
+ if git rev-parse --verify origin/master >/dev/null 2>&1; then
+ DEFAULT_BRANCH="master"
+ fi
+fi
+
+# --- 1c. Get Changed Files ---
+
+CHANGED_FILES=()
+MODE=""
+
+if [[ -n "$CURRENT_BRANCH" && -n "$DEFAULT_BRANCH" && "$CURRENT_BRANCH" != "$DEFAULT_BRANCH" ]]; then
+ # Mode A β Feature Branch
+ MERGE_BASE=$(git merge-base "origin/$DEFAULT_BRANCH" HEAD 2>/dev/null || echo "")
+
+ if [[ -n "$MERGE_BASE" ]]; then
+ # Committed changes since merge-base
+ COMMITTED=()
+ while IFS= read -r -d '' line; do
+ [[ -n "$line" ]] && COMMITTED+=("$line")
+ done < <(git diff --name-only -z --diff-filter=ACMR "${MERGE_BASE}...HEAD" 2>/dev/null)
+
+ # Staged (index) changes
+ STAGED=()
+ while IFS= read -r -d '' line; do
+ [[ -n "$line" ]] && STAGED+=("$line")
+ done < <(git diff --cached --name-only -z --diff-filter=ACMR 2>/dev/null)
+
+ # Unstaged (working tree) changes
+ UNSTAGED=()
+ while IFS= read -r -d '' line; do
+ [[ -n "$line" ]] && UNSTAGED+=("$line")
+ done < <(git diff --name-only -z --diff-filter=ACMR 2>/dev/null)
+
+ # Combine and deduplicate (bash 3 compatible β no associative arrays)
+ CHANGED_FILES=()
+ for f in "${COMMITTED[@]}" "${STAGED[@]}" "${UNSTAGED[@]}"; do
+ [[ -z "$f" ]] && continue
+ _dup=false
+ for existing in "${CHANGED_FILES[@]}"; do
+ if [[ "$existing" == "$f" ]]; then
+ _dup=true
+ break
+ fi
+ done
+ if ! $_dup; then
+ CHANGED_FILES+=("$f")
+ fi
+ done
+
+ MODE="Feature branch diff (${DEFAULT_BRANCH}...HEAD) + uncommitted changes"
+ else
+ # merge-base failed β fall through to Mode B
+ DEFAULT_BRANCH=""
+ fi
+fi
+
+if [[ -z "$MODE" ]]; then
+ # Mode B β Working Directory Changes
+ STAGED=()
+ while IFS= read -r -d '' line; do
+ [[ -n "$line" ]] && STAGED+=("$line")
+ done < <(git diff --cached --name-only -z --diff-filter=ACMR 2>/dev/null)
+
+ UNSTAGED=()
+ while IFS= read -r -d '' line; do
+ [[ -n "$line" ]] && UNSTAGED+=("$line")
+ done < <(git diff --name-only -z --diff-filter=ACMR 2>/dev/null)
+
+ # Combine and deduplicate (bash 3 compatible β no associative arrays)
+ CHANGED_FILES=()
+ for f in "${STAGED[@]}" "${UNSTAGED[@]}"; do
+ [[ -z "$f" ]] && continue
+ _dup=false
+ for existing in "${CHANGED_FILES[@]}"; do
+ if [[ "$existing" == "$f" ]]; then
+ _dup=true
+ break
+ fi
+ done
+ if ! $_dup; then
+ CHANGED_FILES+=("$f")
+ fi
+ done
+
+ MODE="Working directory changes (staged + unstaged)"
+ [[ -z "$DEFAULT_BRANCH" ]] && DEFAULT_BRANCH="(unknown)"
+fi
+
+# --- 1d. Validate Changed Files ---
+if [[ ${#CHANGED_FILES[@]} -eq 0 ]]; then
+ if $JSON_MODE; then
+ printf '{"branch":"%s","default_branch":"%s","mode":"%s","changed_files":[],"message":"No changes detected. Nothing to review."}\n' \
+ "$(json_escape "$CURRENT_BRANCH")" "$(json_escape "$DEFAULT_BRANCH")" "$(json_escape "$MODE")"
+ else
+ echo "No changes detected. Nothing to review."
+ fi
+ exit 2
+fi
+
+# --- Output ---
+if $JSON_MODE; then
+ printf '{"branch":"%s","default_branch":"%s","mode":"%s","changed_files":%s}\n' \
+ "$(json_escape "$CURRENT_BRANCH")" "$(json_escape "$DEFAULT_BRANCH")" "$(json_escape "$MODE")" "$(fmt_array "${CHANGED_FILES[@]}")"
+else
+ echo "BRANCH: $CURRENT_BRANCH"
+ echo "DEFAULT_BRANCH: $DEFAULT_BRANCH"
+ echo "MODE: $MODE"
+ echo "CHANGED_FILES:"
+ for f in "${CHANGED_FILES[@]}"; do
+ echo " $f"
+ done
+fi
+
+exit 0
diff --git a/.specify/extensions/review/scripts/powershell/detect-changed-files.ps1 b/.specify/extensions/review/scripts/powershell/detect-changed-files.ps1
new file mode 100644
index 0000000000..cd71c6de0f
--- /dev/null
+++ b/.specify/extensions/review/scripts/powershell/detect-changed-files.ps1
@@ -0,0 +1,228 @@
+<#
+.SYNOPSIS
+ Detect changed files for code review via git diff.
+
+.DESCRIPTION
+ Identifies changed files by comparing the current branch against
+ the default branch plus any uncommitted work (Mode A - feature branch
+ diff + working directory) or by collecting staged + unstaged changes
+ (Mode B - working directory changes only).
+
+.PARAMETER Json
+ Output in JSON format (for machine consumption).
+
+.PARAMETER Help
+ Show help message and exit.
+
+.EXAMPLE
+ .\detect-changed-files.ps1
+ # Text output of changed files
+
+.EXAMPLE
+ .\detect-changed-files.ps1 -Json
+ # JSON output of changed files
+
+.NOTES
+ EXIT CODES:
+ 0 Changed files detected successfully
+ 1 Error (git unavailable, not a git repository)
+ 2 No changes detected
+#>
+
+[CmdletBinding()]
+param(
+ [switch]$Json,
+ [Alias("h")]
+ [switch]$Help
+)
+
+$ErrorActionPreference = 'Stop'
+
+# --- Help ---
+if ($Help) {
+ @"
+Usage: detect-changed-files.ps1 [OPTIONS]
+
+Detect changed files for code review via git diff.
+
+OPTIONS:
+ -Json Output in JSON format
+ -Help, -h Show this help message
+
+EXIT CODES:
+ 0 Changed files detected successfully
+ 1 Error (git unavailable, not a git repository)
+ 2 No changes detected
+"@
+ exit 0
+}
+
+# --- Helper: output error and exit ---
+function Write-ErrorAndExit {
+ param(
+ [string]$Message,
+ [int]$Code = 1
+ )
+ if ($Json) {
+ [PSCustomObject]@{ error = $Message } | ConvertTo-Json -Compress
+ } else {
+ Write-Error "Error: $Message"
+ }
+ exit $Code
+}
+
+# --- 1a. Verify Git Availability ---
+$gitCmd = Get-Command git -ErrorAction SilentlyContinue
+if (-not $gitCmd) {
+ Write-ErrorAndExit "git is not available. The review extension requires git to identify changed files." 1
+}
+
+$gitDir = git rev-parse --git-dir 2>&1
+if ($LASTEXITCODE -ne 0) {
+ Write-ErrorAndExit "Not a git repository. The review extension requires git to identify changed files." 1
+}
+
+# --- 1b. Detect Branch Context ---
+
+# Get current branch (empty string if detached HEAD)
+$CurrentBranch = ""
+try {
+ $CurrentBranch = (git branch --show-current 2>$null) | Out-String
+ $CurrentBranch = $CurrentBranch.Trim()
+} catch {
+ $CurrentBranch = ""
+}
+
+# Determine default branch
+$DefaultBranch = ""
+
+# Try symbolic-ref first
+try {
+ $symref = (git symbolic-ref refs/remotes/origin/HEAD 2>$null) | Out-String
+ $symref = $symref.Trim()
+ if ($symref -match "refs/remotes/origin/(.+)$") {
+ $DefaultBranch = $Matches[1]
+ }
+} catch {}
+
+# Fallback: check origin/main
+if (-not $DefaultBranch) {
+ $null = git rev-parse --verify origin/main 2>$null
+ if ($LASTEXITCODE -eq 0) {
+ $DefaultBranch = "main"
+ }
+}
+
+# Fallback: check origin/master
+if (-not $DefaultBranch) {
+ $null = git rev-parse --verify origin/master 2>$null
+ if ($LASTEXITCODE -eq 0) {
+ $DefaultBranch = "master"
+ }
+}
+
+# --- 1c. Get Changed Files ---
+
+$ChangedFiles = @()
+$Mode = ""
+
+if ($CurrentBranch -and $DefaultBranch -and ($CurrentBranch -ne $DefaultBranch)) {
+ # Mode A - Feature Branch
+ $MergeBase = ""
+ try {
+ $MergeBase = (git merge-base "origin/$DefaultBranch" HEAD 2>$null) | Out-String
+ $MergeBase = $MergeBase.Trim()
+ } catch {}
+
+ if ($MergeBase) {
+ # Committed changes since merge-base
+ $diffRaw = git diff --name-only -z --diff-filter=ACMR "$MergeBase...HEAD" 2>$null
+ $committedFiles = @()
+ if ($diffRaw) {
+ $diffJoined = ($diffRaw -join "`n")
+ $committedFiles = @($diffJoined -split "`0" | Where-Object { $_ -ne "" })
+ }
+
+ # Staged (index) changes
+ $stagedRaw = git diff --cached --name-only -z --diff-filter=ACMR 2>$null
+ $stagedFiles = @()
+ if ($stagedRaw) {
+ $sJoined = ($stagedRaw -join "`n")
+ $stagedFiles = @($sJoined -split "`0" | Where-Object { $_ -ne "" })
+ }
+
+ # Unstaged (working tree) changes
+ $unstagedRaw = git diff --name-only -z --diff-filter=ACMR 2>$null
+ $unstagedFiles = @()
+ if ($unstagedRaw) {
+ $uJoined = ($unstagedRaw -join "`n")
+ $unstagedFiles = @($uJoined -split "`0" | Where-Object { $_ -ne "" })
+ }
+
+ # Combine and deduplicate
+ $ChangedFiles = @($committedFiles + $stagedFiles + $unstagedFiles | Sort-Object -Unique)
+
+ $Mode = "Feature branch diff ($DefaultBranch...HEAD) + uncommitted changes"
+ } else {
+ # merge-base failed - fall through to Mode B
+ $DefaultBranch = ""
+ }
+}
+
+if (-not $Mode) {
+ # Mode B - Working Directory Changes
+ $stagedRaw = git diff --cached --name-only -z --diff-filter=ACMR 2>$null
+ $unstagedRaw = git diff --name-only -z --diff-filter=ACMR 2>$null
+
+ $allFiles = @()
+ if ($stagedRaw) {
+ $sJoined = ($stagedRaw -join "`n")
+ $allFiles += @($sJoined -split "`0" | Where-Object { $_ -ne "" })
+ }
+ if ($unstagedRaw) {
+ $uJoined = ($unstagedRaw -join "`n")
+ $allFiles += @($uJoined -split "`0" | Where-Object { $_ -ne "" })
+ }
+
+ # Deduplicate
+ $ChangedFiles = @($allFiles | Sort-Object -Unique)
+
+ $Mode = "Working directory changes (staged + unstaged)"
+ if (-not $DefaultBranch) { $DefaultBranch = "(unknown)" }
+}
+
+# --- 1d. Validate Changed Files ---
+if ($ChangedFiles.Count -eq 0) {
+ if ($Json) {
+ [PSCustomObject]@{
+ branch = $CurrentBranch
+ default_branch = $DefaultBranch
+ mode = $Mode
+ changed_files = @()
+ message = "No changes detected. Nothing to review."
+ } | ConvertTo-Json -Compress
+ } else {
+ Write-Output "No changes detected. Nothing to review."
+ }
+ exit 2
+}
+
+# --- Output ---
+if ($Json) {
+ [PSCustomObject]@{
+ branch = $CurrentBranch
+ default_branch = $DefaultBranch
+ mode = $Mode
+ changed_files = $ChangedFiles
+ } | ConvertTo-Json -Compress
+} else {
+ Write-Output "BRANCH: $CurrentBranch"
+ Write-Output "DEFAULT_BRANCH: $DefaultBranch"
+ Write-Output "MODE: $Mode"
+ Write-Output "CHANGED_FILES:"
+ foreach ($f in $ChangedFiles) {
+ Write-Output " $f"
+ }
+}
+
+exit 0
diff --git a/.specify/extensions/review/tests/bats/detect-changed-files.bats b/.specify/extensions/review/tests/bats/detect-changed-files.bats
new file mode 100644
index 0000000000..4457eeaaf7
--- /dev/null
+++ b/.specify/extensions/review/tests/bats/detect-changed-files.bats
@@ -0,0 +1,650 @@
+load "test_helper"
+
+setup() {
+ setup_temp_dir
+}
+
+teardown() {
+ teardown_temp_dir
+}
+
+# ββββββββββββββββββββββββββββββββββββββββββββββ
+# 1a. Git Availability Errors
+# ββββββββββββββββββββββββββββββββββββββββββββββ
+
+@test "fails when not in a git repository" {
+ cd "$TEST_TEMP_DIR"
+ run bash "$SCRIPTS_DIR/detect-changed-files.sh"
+ assert_failure
+ [ "$status" -eq 1 ]
+ assert_output --partial "Not a git repository"
+}
+
+@test "fails with JSON error when not in a git repository" {
+ cd "$TEST_TEMP_DIR"
+ run bash "$SCRIPTS_DIR/detect-changed-files.sh" --json
+ assert_failure
+ [ "$status" -eq 1 ]
+ assert_output --partial '"error"'
+ assert_output --partial "Not a git repository"
+}
+
+# ββββββββββββββββββββββββββββββββββββββββββββββ
+# Help
+# ββββββββββββββββββββββββββββββββββββββββββββββ
+
+@test "--help shows usage information" {
+ run bash "$SCRIPTS_DIR/detect-changed-files.sh" --help
+ assert_success
+ assert_output --partial "Usage"
+ assert_output --partial "detect-changed-files"
+}
+
+@test "-h shows usage information" {
+ run bash "$SCRIPTS_DIR/detect-changed-files.sh" -h
+ assert_success
+ assert_output --partial "Usage"
+}
+
+@test "unknown option fails with error" {
+ run bash "$SCRIPTS_DIR/detect-changed-files.sh" --bogus
+ assert_failure
+ assert_output --partial "Unknown option"
+}
+
+# ββββββββββββββββββββββββββββββββββββββββββββββ
+# No Changes Detected (exit code 2)
+# ββββββββββββββββββββββββββββββββββββββββββββββ
+
+@test "exit code 2 when no changes in clean repo" {
+ init_git_repo "$TEST_TEMP_DIR"
+ cd "$TEST_TEMP_DIR"
+ run bash "$SCRIPTS_DIR/detect-changed-files.sh"
+ [ "$status" -eq 2 ]
+ assert_output --partial "No changes detected"
+}
+
+@test "exit code 2 with JSON output when no changes" {
+ init_git_repo "$TEST_TEMP_DIR"
+ cd "$TEST_TEMP_DIR"
+ run bash "$SCRIPTS_DIR/detect-changed-files.sh" --json
+ [ "$status" -eq 2 ]
+ assert_valid_json "$output"
+ local msg=$(json_field "$output" "message")
+ [[ "$msg" == *"No changes detected"* ]]
+}
+
+# ββββββββββββββββββββββββββββββββββββββββββββββ
+# Mode B β Working Directory: Unstaged Changes
+# ββββββββββββββββββββββββββββββββββββββββββββββ
+
+@test "detects unstaged changes (Mode B)" {
+ init_git_repo "$TEST_TEMP_DIR"
+ cd "$TEST_TEMP_DIR"
+
+ # Create and commit a file, then modify it
+ echo "initial" > tracked.txt
+ git add tracked.txt
+ git commit --quiet -m "Add tracked file"
+ echo "modified" > tracked.txt
+
+ run bash "$SCRIPTS_DIR/detect-changed-files.sh"
+ assert_success
+ assert_output --partial "tracked.txt"
+ assert_output --partial "Working directory changes"
+}
+
+@test "detects unstaged changes with --json (Mode B)" {
+ init_git_repo "$TEST_TEMP_DIR"
+ cd "$TEST_TEMP_DIR"
+
+ echo "initial" > tracked.txt
+ git add tracked.txt
+ git commit --quiet -m "Add tracked file"
+ echo "modified" > tracked.txt
+
+ run bash "$SCRIPTS_DIR/detect-changed-files.sh" --json
+ assert_success
+ assert_valid_json "$output"
+ local mode=$(json_field "$output" "mode")
+ [[ "$mode" == "Working directory changes (staged + unstaged)" ]]
+ assert_output --partial '"tracked.txt"'
+}
+
+# ββββββββββββββββββββββββββββββββββββββββββββββ
+# Mode B β Working Directory: Staged Changes
+# ββββββββββββββββββββββββββββββββββββββββββββββ
+
+@test "detects staged changes (Mode B)" {
+ init_git_repo "$TEST_TEMP_DIR"
+ cd "$TEST_TEMP_DIR"
+
+ echo "new file" > staged.txt
+ git add staged.txt
+
+ run bash "$SCRIPTS_DIR/detect-changed-files.sh"
+ assert_success
+ assert_output --partial "staged.txt"
+ assert_output --partial "Working directory changes"
+}
+
+# ββββββββββββββββββββββββββββββββββββββββββββββ
+# Mode B β Staged + Unstaged Deduplication
+# ββββββββββββββββββββββββββββββββββββββββββββββ
+
+@test "deduplicates staged and unstaged changes (Mode B)" {
+ init_git_repo "$TEST_TEMP_DIR"
+ cd "$TEST_TEMP_DIR"
+
+ # Create a file, commit, stage a change, then modify again (unstaged)
+ echo "v1" > both.txt
+ git add both.txt
+ git commit --quiet -m "Add both.txt"
+ echo "v2" > both.txt
+ git add both.txt
+ echo "v3" > both.txt
+
+ # Also add a new staged-only file
+ echo "new" > only-staged.txt
+ git add only-staged.txt
+
+ run bash "$SCRIPTS_DIR/detect-changed-files.sh" --json
+ assert_success
+ assert_valid_json "$output"
+
+ # both.txt should appear only once
+ local count=$(echo "$output" | python3 -c "import json,sys; print(json.load(sys.stdin)['changed_files'].count('both.txt'))")
+ [ "$count" -eq 1 ]
+
+ # only-staged.txt should also be present
+ assert_output --partial '"only-staged.txt"'
+}
+
+# ββββββββββββββββββββββββββββββββββββββββββββββ
+# Mode A β Feature Branch Diff
+# ββββββββββββββββββββββββββββββββββββββββββββββ
+
+@test "detects feature branch changes via merge-base (Mode A)" {
+ init_git_repo_with_remote "$TEST_TEMP_DIR"
+ cd "$TEST_TEMP_DIR"
+
+ # Create a feature branch with a new file
+ git checkout --quiet -b feature-branch
+ echo "feature code" > feature.txt
+ git add feature.txt
+ git commit --quiet -m "Add feature file"
+
+ run bash "$SCRIPTS_DIR/detect-changed-files.sh"
+ assert_success
+ assert_output --partial "feature.txt"
+ assert_output --partial "Feature branch diff"
+}
+
+@test "Mode A --json has correct structure" {
+ init_git_repo_with_remote "$TEST_TEMP_DIR"
+ cd "$TEST_TEMP_DIR"
+
+ git checkout --quiet -b feature-branch
+ echo "feature code" > feature.txt
+ git add feature.txt
+ git commit --quiet -m "Add feature file"
+
+ run bash "$SCRIPTS_DIR/detect-changed-files.sh" --json
+ assert_success
+ assert_valid_json "$output"
+
+ local branch=$(json_field "$output" "branch")
+ [ "$branch" = "feature-branch" ]
+
+ local mode=$(json_field "$output" "mode")
+ [[ "$mode" == "Feature branch diff"* ]]
+
+ assert_output --partial '"feature.txt"'
+}
+
+@test "Mode A excludes deleted files (diff-filter=ACMR)" {
+ init_git_repo_with_remote "$TEST_TEMP_DIR"
+ cd "$TEST_TEMP_DIR"
+
+ # Add a file on main and push
+ echo "to delete" > delete-me.txt
+ git add delete-me.txt
+ git commit --quiet -m "Add file to delete"
+ git push --quiet origin main
+
+ # Create feature branch, delete the file, add another
+ git checkout --quiet -b feature-delete
+ git rm --quiet delete-me.txt
+ echo "keep me" > keep.txt
+ git add keep.txt
+ git commit --quiet -m "Delete and add"
+
+ run bash "$SCRIPTS_DIR/detect-changed-files.sh" --json
+ assert_success
+
+ # delete-me.txt should NOT appear (it was deleted)
+ local files=$(json_array_field "$output" "changed_files")
+ ! echo "$files" | grep -q "delete-me.txt"
+
+ # keep.txt should appear
+ assert_output --partial '"keep.txt"'
+}
+
+@test "Mode A detects multiple changed files" {
+ init_git_repo_with_remote "$TEST_TEMP_DIR"
+ cd "$TEST_TEMP_DIR"
+
+ git checkout --quiet -b multi-files
+ echo "a" > file-a.txt
+ echo "b" > file-b.txt
+ mkdir -p sub
+ echo "c" > sub/file-c.txt
+ git add .
+ git commit --quiet -m "Add multiple files"
+
+ run bash "$SCRIPTS_DIR/detect-changed-files.sh" --json
+ assert_success
+ assert_output --partial '"file-a.txt"'
+ assert_output --partial '"file-b.txt"'
+ assert_output --partial '"sub/file-c.txt"'
+}
+
+# ββββββββββββββββββββββββββββββββββββββββββββββ
+# Mode A β Feature Branch + Uncommitted Changes
+# ββββββββββββββββββββββββββββββββββββββββββββββ
+
+@test "Mode A includes staged uncommitted files" {
+ init_git_repo_with_remote "$TEST_TEMP_DIR"
+ cd "$TEST_TEMP_DIR"
+
+ git checkout --quiet -b feature-staged
+ echo "committed" > committed.txt
+ git add committed.txt
+ git commit --quiet -m "Add committed file"
+
+ # Stage a new file without committing
+ echo "staged" > staged-only.txt
+ git add staged-only.txt
+
+ run bash "$SCRIPTS_DIR/detect-changed-files.sh" --json
+ assert_success
+ assert_valid_json "$output"
+
+ # Both committed and staged files should appear
+ assert_output --partial '"committed.txt"'
+ assert_output --partial '"staged-only.txt"'
+
+ local mode=$(json_field "$output" "mode")
+ [[ "$mode" == *"uncommitted"* ]]
+}
+
+@test "Mode A includes unstaged uncommitted files" {
+ init_git_repo_with_remote "$TEST_TEMP_DIR"
+ cd "$TEST_TEMP_DIR"
+
+ # Create a file on main, push it, then modify on feature branch
+ echo "original" > existing.txt
+ git add existing.txt
+ git commit --quiet -m "Add existing file"
+ git push --quiet origin main
+
+ git checkout --quiet -b feature-unstaged
+ echo "committed on branch" > committed.txt
+ git add committed.txt
+ git commit --quiet -m "Add committed file"
+
+ # Modify existing file without staging
+ echo "modified" > existing.txt
+
+ run bash "$SCRIPTS_DIR/detect-changed-files.sh" --json
+ assert_success
+ assert_valid_json "$output"
+
+ # Both committed diff and unstaged modification should appear
+ assert_output --partial '"committed.txt"'
+ assert_output --partial '"existing.txt"'
+}
+
+@test "Mode A deduplicates committed and uncommitted files" {
+ init_git_repo_with_remote "$TEST_TEMP_DIR"
+ cd "$TEST_TEMP_DIR"
+
+ git checkout --quiet -b feature-dedup
+ echo "v1" > shared.txt
+ git add shared.txt
+ git commit --quiet -m "Add shared file"
+
+ # Modify the same file (unstaged) β it appears in both committed diff and unstaged
+ echo "v2" > shared.txt
+
+ run bash "$SCRIPTS_DIR/detect-changed-files.sh" --json
+ assert_success
+ assert_valid_json "$output"
+
+ # shared.txt should appear exactly once
+ local count=$(echo "$output" | python3 -c "import json,sys; print(json.load(sys.stdin)['changed_files'].count('shared.txt'))")
+ [ "$count" -eq 1 ]
+}
+
+# ββββββββββββββββββββββββββββββββββββββββββββββ
+# Default Branch Detection Fallbacks
+# ββββββββββββββββββββββββββββββββββββββββββββββ
+
+@test "detects origin/main as default branch" {
+ init_git_repo_with_remote "$TEST_TEMP_DIR"
+ cd "$TEST_TEMP_DIR"
+
+ # Unset symbolic-ref to force fallback
+ git remote set-head origin --delete 2>/dev/null || true
+
+ git checkout --quiet -b test-branch
+ echo "test" > test-file.txt
+ git add test-file.txt
+ git commit --quiet -m "Add test file"
+
+ run bash "$SCRIPTS_DIR/detect-changed-files.sh" --json
+ assert_success
+
+ local default=$(json_field "$output" "default_branch")
+ [ "$default" = "main" ]
+}
+
+@test "detects origin/master as default branch when no origin/main" {
+ # Create a bare repo with master as default
+ local bare="${TEST_TEMP_DIR}/_bare_master"
+ mkdir -p "$bare"
+ git -C "$bare" init --bare --quiet
+ git -C "$bare" symbolic-ref HEAD refs/heads/master
+
+ # Clone-like setup
+ cd "$TEST_TEMP_DIR"
+ git init --quiet
+ git config user.email "test@example.com"
+ git config user.name "Test"
+ git remote add origin "$bare"
+
+ # Create initial commit on master and push
+ touch .gitkeep
+ git add .
+ git checkout -b master --quiet 2>/dev/null || true
+ git commit --quiet -m "Initial commit"
+ git push --quiet origin master
+
+ # Remove symbolic-ref to force fallback
+ git remote set-head origin --delete 2>/dev/null || true
+
+ # Create feature branch
+ git checkout --quiet -b test-branch
+ echo "test" > test-file.txt
+ git add test-file.txt
+ git commit --quiet -m "Add test file"
+
+ run bash "$SCRIPTS_DIR/detect-changed-files.sh" --json
+ assert_success
+
+ local default=$(json_field "$output" "default_branch")
+ [ "$default" = "master" ]
+}
+
+@test "falls back to Mode B when no remote default branch found" {
+ init_git_repo "$TEST_TEMP_DIR"
+ cd "$TEST_TEMP_DIR"
+
+ # No remote configured, so no origin/* branches
+ echo "change" > new-file.txt
+ git add new-file.txt
+
+ run bash "$SCRIPTS_DIR/detect-changed-files.sh" --json
+ assert_success
+
+ local mode=$(json_field "$output" "mode")
+ [[ "$mode" == "Working directory changes"* ]]
+}
+
+# ββββββββββββββββββββββββββββββββββββββββββββββ
+# Detached HEAD
+# ββββββββββββββββββββββββββββββββββββββββββββββ
+
+@test "detached HEAD falls back to Mode B" {
+ init_git_repo "$TEST_TEMP_DIR"
+ cd "$TEST_TEMP_DIR"
+
+ # Create a commit and detach HEAD
+ echo "file" > detached.txt
+ git add detached.txt
+ git commit --quiet -m "Add file"
+ local commit_hash=$(git rev-parse HEAD)
+ git checkout --quiet "$commit_hash"
+
+ # Make a change
+ echo "modified" > detached.txt
+
+ run bash "$SCRIPTS_DIR/detect-changed-files.sh"
+ assert_success
+ assert_output --partial "Working directory changes"
+ assert_output --partial "detached.txt"
+}
+
+# ββββββββββββββββββββββββββββββββββββββββββββββ
+# JSON Output Validation
+# ββββββββββββββββββββββββββββββββββββββββββββββ
+
+@test "--json output is valid JSON with all required keys" {
+ init_git_repo "$TEST_TEMP_DIR"
+ cd "$TEST_TEMP_DIR"
+
+ echo "content" > new.txt
+ git add new.txt
+
+ run bash "$SCRIPTS_DIR/detect-changed-files.sh" --json
+ assert_success
+ assert_valid_json "$output"
+
+ # Verify all expected keys exist
+ echo "$output" | python3 -c "
+import json, sys
+data = json.load(sys.stdin)
+assert 'branch' in data, 'missing branch key'
+assert 'default_branch' in data, 'missing default_branch key'
+assert 'mode' in data, 'missing mode key'
+assert 'changed_files' in data, 'missing changed_files key'
+assert isinstance(data['changed_files'], list), 'changed_files should be a list'
+print('All keys present and correct types')
+"
+}
+
+@test "text mode output has correct format" {
+ init_git_repo "$TEST_TEMP_DIR"
+ cd "$TEST_TEMP_DIR"
+
+ echo "content" > formatted.txt
+ git add formatted.txt
+
+ run bash "$SCRIPTS_DIR/detect-changed-files.sh"
+ assert_success
+ assert_output --partial "BRANCH:"
+ assert_output --partial "DEFAULT_BRANCH:"
+ assert_output --partial "MODE:"
+ assert_output --partial "CHANGED_FILES:"
+ assert_output --partial "formatted.txt"
+}
+
+# ββββββββββββββββββββββββββββββββββββββββββββββ
+# Edge Cases
+# ββββββββββββββββββββββββββββββββββββββββββββββ
+
+@test "handles files with spaces in names" {
+ init_git_repo "$TEST_TEMP_DIR"
+ cd "$TEST_TEMP_DIR"
+
+ echo "content" > "file with spaces.txt"
+ git add "file with spaces.txt"
+
+ run bash "$SCRIPTS_DIR/detect-changed-files.sh"
+ assert_success
+ assert_output --partial "file with spaces.txt"
+}
+
+@test "handles nested directory changes" {
+ init_git_repo "$TEST_TEMP_DIR"
+ cd "$TEST_TEMP_DIR"
+
+ mkdir -p deep/nested/path
+ echo "deep" > deep/nested/path/file.txt
+ git add .
+
+ run bash "$SCRIPTS_DIR/detect-changed-files.sh" --json
+ assert_success
+ assert_output --partial '"deep/nested/path/file.txt"'
+}
+
+@test "only reports ACMR files (Added, Copied, Modified, Renamed)" {
+ init_git_repo "$TEST_TEMP_DIR"
+ cd "$TEST_TEMP_DIR"
+
+ # Add and commit files
+ echo "keep" > keep.txt
+ echo "remove" > remove.txt
+ git add .
+ git commit --quiet -m "Add files"
+
+ # Delete one, modify another β only staged
+ git rm --quiet remove.txt
+ echo "modified" > keep.txt
+ echo "added" > added.txt
+ git add .
+
+ run bash "$SCRIPTS_DIR/detect-changed-files.sh" --json
+ assert_success
+
+ # keep.txt (Modified) and added.txt (Added) should be present
+ assert_output --partial '"keep.txt"'
+ assert_output --partial '"added.txt"'
+
+ # remove.txt (Deleted) should NOT be present
+ local files=$(json_array_field "$output" "changed_files")
+ [[ ! "$files" == *"remove.txt"* ]]
+}
+
+@test "Mode A: branch field matches current branch name" {
+ init_git_repo_with_remote "$TEST_TEMP_DIR"
+ cd "$TEST_TEMP_DIR"
+
+ git checkout --quiet -b my-feature-123
+ echo "x" > x.txt
+ git add x.txt
+ git commit --quiet -m "commit"
+
+ run bash "$SCRIPTS_DIR/detect-changed-files.sh" --json
+ assert_success
+
+ local branch=$(json_field "$output" "branch")
+ [ "$branch" = "my-feature-123" ]
+}
+
+@test "Mode B: branch field shows current branch on default" {
+ init_git_repo "$TEST_TEMP_DIR"
+ cd "$TEST_TEMP_DIR"
+
+ echo "change" > file.txt
+ git add file.txt
+
+ run bash "$SCRIPTS_DIR/detect-changed-files.sh" --json
+ assert_success
+
+ # Branch should be main (or master depending on git default)
+ local branch=$(json_field "$output" "branch")
+ [[ "$branch" == "main" || "$branch" == "master" ]]
+}
+
+# ββββββββββββββββββββββββββββββββββββββββββββββ
+# Special Characters in Filenames
+# ββββββββββββββββββββββββββββββββββββββββββββββ
+
+@test "handles filenames with double quotes in JSON mode" {
+ init_git_repo "$TEST_TEMP_DIR"
+ cd "$TEST_TEMP_DIR"
+
+ # Create a file with a double quote in its name
+ local fname='file"quote.txt'
+ echo "content" > "$fname"
+ git add "$fname"
+
+ run bash "$SCRIPTS_DIR/detect-changed-files.sh" --json
+ assert_success
+ assert_valid_json "$output"
+
+ # The filename should be properly escaped in JSON
+ local files=$(json_array_field "$output" "changed_files")
+ [[ "$files" == *'file"quote.txt'* ]]
+}
+
+@test "handles filenames with backslashes in JSON mode" {
+ init_git_repo "$TEST_TEMP_DIR"
+ cd "$TEST_TEMP_DIR"
+
+ # Create a file with a backslash in its name
+ local fname='file\slash.txt'
+ echo "content" > "$fname"
+ git add "$fname"
+
+ run bash "$SCRIPTS_DIR/detect-changed-files.sh" --json
+ assert_success
+ assert_valid_json "$output"
+}
+
+@test "handles filenames with special characters in JSON mode" {
+ init_git_repo "$TEST_TEMP_DIR"
+ cd "$TEST_TEMP_DIR"
+
+ # Create files with various special characters
+ echo "a" > "file (1).txt"
+ echo "b" > "file's.txt"
+ echo "c" > "file&more.txt"
+ git add .
+
+ run bash "$SCRIPTS_DIR/detect-changed-files.sh" --json
+ assert_success
+ assert_valid_json "$output"
+
+ # All three files should be present
+ local count=$(echo "$output" | python3 -c "import json,sys; print(len(json.load(sys.stdin)['changed_files']))")
+ [ "$count" -eq 3 ]
+}
+
+# ββββββββββββββββββββββββββββββββββββββββββββββ
+# Renamed Files
+# ββββββββββββββββββββββββββββββββββββββββββββββ
+
+@test "Mode A detects renamed files (diff-filter includes R)" {
+ init_git_repo_with_remote "$TEST_TEMP_DIR"
+ cd "$TEST_TEMP_DIR"
+
+ echo "content" > original.txt
+ git add original.txt
+ git commit --quiet -m "Add original"
+ git push --quiet origin main
+
+ git checkout --quiet -b feature-rename
+ git mv original.txt renamed.txt
+ git commit --quiet -m "Rename file"
+
+ run bash "$SCRIPTS_DIR/detect-changed-files.sh" --json
+ assert_success
+ assert_output --partial '"renamed.txt"'
+}
+
+@test "Mode B detects renamed files (staged)" {
+ init_git_repo "$TEST_TEMP_DIR"
+ cd "$TEST_TEMP_DIR"
+
+ echo "content" > original.txt
+ git add original.txt
+ git commit --quiet -m "Add original"
+
+ git mv original.txt renamed.txt
+
+ run bash "$SCRIPTS_DIR/detect-changed-files.sh" --json
+ assert_success
+ assert_output --partial '"renamed.txt"'
+}
diff --git a/.specify/extensions/review/tests/bats/test_helper.bash b/.specify/extensions/review/tests/bats/test_helper.bash
new file mode 100644
index 0000000000..c9ac12a129
--- /dev/null
+++ b/.specify/extensions/review/tests/bats/test_helper.bash
@@ -0,0 +1,90 @@
+#!/usr/bin/env bash
+# Shared test helper for BATS tests
+
+# Load bats libraries
+BATS_LIB_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/lib" && pwd)"
+load "${BATS_LIB_DIR}/bats-support/load"
+load "${BATS_LIB_DIR}/bats-assert/load"
+
+# Project root (repo root)
+PROJECT_ROOT="$(cd "$(dirname "$BATS_TEST_FILENAME")/../.." && pwd)"
+
+# Scripts under test
+SCRIPTS_DIR="${PROJECT_ROOT}/scripts/bash"
+
+# Create a temporary working directory for each test
+setup_temp_dir() {
+ TEST_TEMP_DIR="$(mktemp -d)"
+ export TEST_TEMP_DIR
+}
+
+# Clean up the temporary directory
+teardown_temp_dir() {
+ if [[ -n "${TEST_TEMP_DIR:-}" && -d "$TEST_TEMP_DIR" ]]; then
+ rm -rf "$TEST_TEMP_DIR"
+ fi
+}
+
+# Initialize a git repo in the temp directory
+init_git_repo() {
+ local dir="${1:-$TEST_TEMP_DIR}"
+ git -C "$dir" init --quiet -b main
+ git -C "$dir" config user.email "test@example.com"
+ git -C "$dir" config user.name "Test"
+ # Create initial commit so git diff works
+ touch "$dir/.gitkeep"
+ git -C "$dir" add .
+ git -C "$dir" commit --quiet -m "Initial commit"
+}
+
+# Initialize a git repo with a bare remote (for origin/* refs)
+init_git_repo_with_remote() {
+ local dir="${1:-$TEST_TEMP_DIR}"
+ local bare_dir="${dir}/_bare_remote"
+
+ # Create a bare repo to act as origin (explicitly use main)
+ mkdir -p "$bare_dir"
+ git -C "$bare_dir" init --bare --quiet
+ git -C "$bare_dir" symbolic-ref HEAD refs/heads/main
+
+ # Create the working repo
+ git -C "$dir" init --quiet -b main
+ git -C "$dir" config user.email "test@example.com"
+ git -C "$dir" config user.name "Test"
+ git -C "$dir" remote add origin "$bare_dir"
+
+ # Create initial commit and push to establish origin/main
+ touch "$dir/.gitkeep"
+ git -C "$dir" add .
+ git -C "$dir" commit --quiet -m "Initial commit"
+ git -C "$dir" push --quiet origin main
+
+ # Set origin/HEAD so symbolic-ref works
+ git -C "$dir" remote set-head origin --auto 2>/dev/null || true
+}
+
+# Validate that output is valid JSON
+assert_valid_json() {
+ local output="$1"
+ echo "$output" | python3 -m json.tool > /dev/null 2>&1 \
+ || fail "Invalid JSON: $output"
+}
+
+# Extract a JSON field value (simple top-level string/bool/number)
+json_field() {
+ local json="$1"
+ local field="$2"
+ echo "$json" | python3 -c "import json,sys; print(json.load(sys.stdin).get('$field',''))"
+}
+
+# Extract a JSON array field as newline-separated values
+json_array_field() {
+ local json="$1"
+ local field="$2"
+ echo "$json" | python3 -c "
+import json, sys
+data = json.load(sys.stdin)
+for item in data.get('$field', []):
+ print(item)
+"
+}
diff --git a/.specify/extensions/review/tests/pester/detect-changed-files.Tests.ps1 b/.specify/extensions/review/tests/pester/detect-changed-files.Tests.ps1
new file mode 100644
index 0000000000..4e3576aca3
--- /dev/null
+++ b/.specify/extensions/review/tests/pester/detect-changed-files.Tests.ps1
@@ -0,0 +1,740 @@
+BeforeAll {
+ $ScriptsDir = Join-Path $PSScriptRoot "..\..\scripts\powershell"
+ $Script = Join-Path $ScriptsDir "detect-changed-files.ps1"
+
+ function New-TempDir {
+ $tmp = Join-Path ([System.IO.Path]::GetTempPath()) ([System.IO.Path]::GetRandomFileName())
+ New-Item -ItemType Directory -Path $tmp -Force | Out-Null
+ return $tmp
+ }
+
+ function Initialize-GitRepo {
+ param([string]$Dir)
+ Push-Location $Dir
+ git init --quiet -b main
+ git config user.email "test@example.com"
+ git config user.name "Test"
+ New-Item -ItemType File -Path ".gitkeep" -Force | Out-Null
+ git add .
+ git commit --quiet -m "Initial commit"
+ Pop-Location
+ }
+
+ function Initialize-GitRepoWithRemote {
+ param([string]$Dir)
+ $bareDir = Join-Path $Dir "_bare_remote"
+ New-Item -ItemType Directory -Path $bareDir -Force | Out-Null
+ Push-Location $bareDir
+ git init --bare --quiet
+ git symbolic-ref HEAD refs/heads/main
+ Pop-Location
+
+ Push-Location $Dir
+ git init --quiet -b main
+ git config user.email "test@example.com"
+ git config user.name "Test"
+ git remote add origin $bareDir
+ New-Item -ItemType File -Path ".gitkeep" -Force | Out-Null
+ git add .
+ git commit --quiet -m "Initial commit"
+ git push --quiet origin main
+ git remote set-head origin --auto 2>$null
+ Pop-Location
+ }
+}
+
+Describe "detect-changed-files.ps1" {
+
+ # ββββββββββββββββββββββββββββββββββββββββββββββ
+ # Help
+ # ββββββββββββββββββββββββββββββββββββββββββββββ
+
+ Describe "Help" {
+ It "shows usage with -Help" {
+ $result = & pwsh -NoProfile -File $Script -Help
+ $LASTEXITCODE | Should -Be 0
+ ($result -join "`n") | Should -Match "Usage"
+ }
+
+ It "shows usage with -h" {
+ $result = & pwsh -NoProfile -File $Script -h
+ $LASTEXITCODE | Should -Be 0
+ ($result -join "`n") | Should -Match "Usage"
+ }
+ }
+
+ # ββββββββββββββββββββββββββββββββββββββββββββββ
+ # Git Availability Errors
+ # ββββββββββββββββββββββββββββββββββββββββββββββ
+
+ Describe "Git availability" {
+ It "fails when not in a git repository" {
+ $tmp = New-TempDir
+ try {
+ Push-Location $tmp
+ $result = & pwsh -NoProfile -File $Script 2>&1
+ $LASTEXITCODE | Should -Be 1
+ } finally {
+ Pop-Location
+ Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue
+ }
+ }
+
+ It "fails with JSON error when not in a git repository" {
+ $tmp = New-TempDir
+ try {
+ Push-Location $tmp
+ $result = & pwsh -NoProfile -File $Script -Json 2>&1
+ $LASTEXITCODE | Should -Be 1
+ ($result -join "`n") | Should -Match '"error"'
+ } finally {
+ Pop-Location
+ Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue
+ }
+ }
+ }
+
+ # ββββββββββββββββββββββββββββββββββββββββββββββ
+ # No Changes Detected
+ # ββββββββββββββββββββββββββββββββββββββββββββββ
+
+ Describe "No changes" {
+ It "exit code 2 when no changes in clean repo" {
+ $tmp = New-TempDir
+ try {
+ Initialize-GitRepo -Dir $tmp
+ Push-Location $tmp
+ & pwsh -NoProfile -File $Script 2>&1
+ $LASTEXITCODE | Should -Be 2
+ } finally {
+ Pop-Location
+ Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue
+ }
+ }
+
+ It "exit code 2 with JSON output when no changes" {
+ $tmp = New-TempDir
+ try {
+ Initialize-GitRepo -Dir $tmp
+ Push-Location $tmp
+ $result = & pwsh -NoProfile -File $Script -Json 2>&1
+ $LASTEXITCODE | Should -Be 2
+ $json = $result | ConvertFrom-Json
+ $json.message | Should -Match "No changes detected"
+ } finally {
+ Pop-Location
+ Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue
+ }
+ }
+ }
+
+ # ββββββββββββββββββββββββββββββββββββββββββββββ
+ # Mode B β Unstaged Changes
+ # ββββββββββββββββββββββββββββββββββββββββββββββ
+
+ Describe "Mode B - Unstaged" {
+ It "detects unstaged changes" {
+ $tmp = New-TempDir
+ try {
+ Initialize-GitRepo -Dir $tmp
+ Push-Location $tmp
+ "initial" | Set-Content "tracked.txt"
+ git add tracked.txt
+ git commit --quiet -m "Add tracked file"
+ "modified" | Set-Content "tracked.txt"
+
+ $result = & pwsh -NoProfile -File $Script -Json 2>&1
+ $LASTEXITCODE | Should -Be 0
+ $json = $result | ConvertFrom-Json
+ $json.mode | Should -Match "Working directory changes"
+ $json.changed_files | Should -Contain "tracked.txt"
+ } finally {
+ Pop-Location
+ Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue
+ }
+ }
+ }
+
+ # ββββββββββββββββββββββββββββββββββββββββββββββ
+ # Mode B β Staged Changes
+ # ββββββββββββββββββββββββββββββββββββββββββββββ
+
+ Describe "Mode B - Staged" {
+ It "detects staged changes" {
+ $tmp = New-TempDir
+ try {
+ Initialize-GitRepo -Dir $tmp
+ Push-Location $tmp
+ "new file" | Set-Content "staged.txt"
+ git add staged.txt
+
+ $result = & pwsh -NoProfile -File $Script -Json 2>&1
+ $LASTEXITCODE | Should -Be 0
+ $json = $result | ConvertFrom-Json
+ $json.mode | Should -Match "Working directory changes"
+ $json.changed_files | Should -Contain "staged.txt"
+ } finally {
+ Pop-Location
+ Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue
+ }
+ }
+ }
+
+ # ββββββββββββββββββββββββββββββββββββββββββββββ
+ # Mode B β Deduplication
+ # ββββββββββββββββββββββββββββββββββββββββββββββ
+
+ Describe "Mode B - Deduplication" {
+ It "deduplicates staged and unstaged changes" {
+ $tmp = New-TempDir
+ try {
+ Initialize-GitRepo -Dir $tmp
+ Push-Location $tmp
+ "v1" | Set-Content "both.txt"
+ git add both.txt
+ git commit --quiet -m "Add both.txt"
+ "v2" | Set-Content "both.txt"
+ git add both.txt
+ "v3" | Set-Content "both.txt"
+
+ $result = & pwsh -NoProfile -File $Script -Json 2>&1
+ $LASTEXITCODE | Should -Be 0
+ $json = $result | ConvertFrom-Json
+ ($json.changed_files | Where-Object { $_ -eq "both.txt" }).Count | Should -Be 1
+ } finally {
+ Pop-Location
+ Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue
+ }
+ }
+ }
+
+ # ββββββββββββββββββββββββββββββββββββββββββββββ
+ # Mode A β Feature Branch Diff
+ # ββββββββββββββββββββββββββββββββββββββββββββββ
+
+ Describe "Mode A - Feature Branch" {
+ It "detects feature branch changes via merge-base" {
+ $tmp = New-TempDir
+ try {
+ Initialize-GitRepoWithRemote -Dir $tmp
+ Push-Location $tmp
+ git checkout --quiet -b feature-branch
+ "feature code" | Set-Content "feature.txt"
+ git add feature.txt
+ git commit --quiet -m "Add feature file"
+
+ $result = & pwsh -NoProfile -File $Script -Json 2>&1
+ $LASTEXITCODE | Should -Be 0
+ $json = $result | ConvertFrom-Json
+ $json.branch | Should -Be "feature-branch"
+ $json.mode | Should -Match "Feature branch diff"
+ $json.changed_files | Should -Contain "feature.txt"
+ } finally {
+ Pop-Location
+ Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue
+ }
+ }
+
+ It "excludes deleted files (diff-filter=ACMR)" {
+ $tmp = New-TempDir
+ try {
+ Initialize-GitRepoWithRemote -Dir $tmp
+ Push-Location $tmp
+ "to delete" | Set-Content "delete-me.txt"
+ git add delete-me.txt
+ git commit --quiet -m "Add file to delete"
+ git push --quiet origin main
+
+ git checkout --quiet -b feature-delete
+ git rm --quiet delete-me.txt
+ "keep me" | Set-Content "keep.txt"
+ git add keep.txt
+ git commit --quiet -m "Delete and add"
+
+ $result = & pwsh -NoProfile -File $Script -Json 2>&1
+ $LASTEXITCODE | Should -Be 0
+ $json = $result | ConvertFrom-Json
+ $json.changed_files | Should -Contain "keep.txt"
+ $json.changed_files | Should -Not -Contain "delete-me.txt"
+ } finally {
+ Pop-Location
+ Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue
+ }
+ }
+
+ It "detects multiple changed files" {
+ $tmp = New-TempDir
+ try {
+ Initialize-GitRepoWithRemote -Dir $tmp
+ Push-Location $tmp
+ git checkout --quiet -b multi-files
+ "a" | Set-Content "file-a.txt"
+ "b" | Set-Content "file-b.txt"
+ $subDir = Join-Path $tmp "sub"
+ New-Item -ItemType Directory -Path $subDir -Force | Out-Null
+ "c" | Set-Content (Join-Path $subDir "file-c.txt")
+ git add .
+ git commit --quiet -m "Add multiple files"
+
+ $result = & pwsh -NoProfile -File $Script -Json 2>&1
+ $LASTEXITCODE | Should -Be 0
+ $json = $result | ConvertFrom-Json
+ $json.changed_files | Should -Contain "file-a.txt"
+ $json.changed_files | Should -Contain "file-b.txt"
+ $json.changed_files | Should -Contain "sub/file-c.txt"
+ } finally {
+ Pop-Location
+ Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue
+ }
+ }
+
+ It "detects renamed files (diff-filter includes R)" {
+ $tmp = New-TempDir
+ try {
+ Initialize-GitRepoWithRemote -Dir $tmp
+ Push-Location $tmp
+ "content" | Set-Content "original.txt"
+ git add original.txt
+ git commit --quiet -m "Add original"
+ git push --quiet origin main
+
+ git checkout --quiet -b feature-rename
+ git mv original.txt renamed.txt
+ git commit --quiet -m "Rename file"
+
+ $result = & pwsh -NoProfile -File $Script -Json 2>&1
+ $LASTEXITCODE | Should -Be 0
+ $json = $result | ConvertFrom-Json
+ $json.changed_files | Should -Contain "renamed.txt"
+ } finally {
+ Pop-Location
+ Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue
+ }
+ }
+ }
+
+ # ββββββββββββββββββββββββββββββββββββββββββββββ
+ # Mode A β Feature Branch + Uncommitted Changes
+ # ββββββββββββββββββββββββββββββββββββββββββββββ
+
+ Describe "Mode A - Uncommitted changes" {
+ It "includes staged uncommitted files" {
+ $tmp = New-TempDir
+ try {
+ Initialize-GitRepoWithRemote -Dir $tmp
+ Push-Location $tmp
+ git checkout --quiet -b feature-staged
+ "committed" | Set-Content "committed.txt"
+ git add committed.txt
+ git commit --quiet -m "Add committed file"
+
+ # Stage a new file without committing
+ "staged" | Set-Content "staged-only.txt"
+ git add staged-only.txt
+
+ $result = & pwsh -NoProfile -File $Script -Json 2>&1
+ $LASTEXITCODE | Should -Be 0
+ $json = $result | ConvertFrom-Json
+ $json.changed_files | Should -Contain "committed.txt"
+ $json.changed_files | Should -Contain "staged-only.txt"
+ $json.mode | Should -Match "uncommitted"
+ } finally {
+ Pop-Location
+ Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue
+ }
+ }
+
+ It "includes unstaged uncommitted files" {
+ $tmp = New-TempDir
+ try {
+ Initialize-GitRepoWithRemote -Dir $tmp
+ Push-Location $tmp
+ "original" | Set-Content "existing.txt"
+ git add existing.txt
+ git commit --quiet -m "Add existing file"
+ git push --quiet origin main
+
+ git checkout --quiet -b feature-unstaged
+ "committed on branch" | Set-Content "committed.txt"
+ git add committed.txt
+ git commit --quiet -m "Add committed file"
+
+ # Modify existing file without staging
+ "modified" | Set-Content "existing.txt"
+
+ $result = & pwsh -NoProfile -File $Script -Json 2>&1
+ $LASTEXITCODE | Should -Be 0
+ $json = $result | ConvertFrom-Json
+ $json.changed_files | Should -Contain "committed.txt"
+ $json.changed_files | Should -Contain "existing.txt"
+ } finally {
+ Pop-Location
+ Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue
+ }
+ }
+
+ It "deduplicates committed and uncommitted files" {
+ $tmp = New-TempDir
+ try {
+ Initialize-GitRepoWithRemote -Dir $tmp
+ Push-Location $tmp
+ git checkout --quiet -b feature-dedup
+ "v1" | Set-Content "shared.txt"
+ git add shared.txt
+ git commit --quiet -m "Add shared file"
+
+ # Modify the same file (unstaged)
+ "v2" | Set-Content "shared.txt"
+
+ $result = & pwsh -NoProfile -File $Script -Json 2>&1
+ $LASTEXITCODE | Should -Be 0
+ $json = $result | ConvertFrom-Json
+ ($json.changed_files | Where-Object { $_ -eq "shared.txt" }).Count | Should -Be 1
+ } finally {
+ Pop-Location
+ Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue
+ }
+ }
+ }
+
+ # ββββββββββββββββββββββββββββββββββββββββββββββ
+ # Default Branch Detection Fallbacks
+ # ββββββββββββββββββββββββββββββββββββββββββββββ
+
+ Describe "Default branch detection" {
+ It "detects origin/main as default branch" {
+ $tmp = New-TempDir
+ try {
+ Initialize-GitRepoWithRemote -Dir $tmp
+ Push-Location $tmp
+
+ # Unset symbolic-ref to force fallback
+ git remote set-head origin --delete 2>$null
+
+ git checkout --quiet -b test-branch
+ "test" | Set-Content "test-file.txt"
+ git add test-file.txt
+ git commit --quiet -m "Add test file"
+
+ $result = & pwsh -NoProfile -File $Script -Json 2>&1
+ $LASTEXITCODE | Should -Be 0
+ $json = $result | ConvertFrom-Json
+ $json.default_branch | Should -Be "main"
+ } finally {
+ Pop-Location
+ Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue
+ }
+ }
+
+ It "falls back to Mode B when no remote default branch found" {
+ $tmp = New-TempDir
+ try {
+ Initialize-GitRepo -Dir $tmp
+ Push-Location $tmp
+ "change" | Set-Content "new-file.txt"
+ git add new-file.txt
+
+ $result = & pwsh -NoProfile -File $Script -Json 2>&1
+ $LASTEXITCODE | Should -Be 0
+ $json = $result | ConvertFrom-Json
+ $json.mode | Should -Match "Working directory changes"
+ } finally {
+ Pop-Location
+ Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue
+ }
+ }
+
+ It "detects origin/master as default branch when no origin/main" {
+ $tmp = New-TempDir
+ try {
+ # Create a bare repo with master as default
+ $bareDir = Join-Path $tmp "_bare_master"
+ New-Item -ItemType Directory -Path $bareDir -Force | Out-Null
+ Push-Location $bareDir
+ git init --bare --quiet
+ git symbolic-ref HEAD refs/heads/master
+ Pop-Location
+
+ Push-Location $tmp
+ git init --quiet
+ git config user.email "test@example.com"
+ git config user.name "Test"
+ git remote add origin $bareDir
+
+ New-Item -ItemType File -Path ".gitkeep" -Force | Out-Null
+ git add .
+ git checkout -b master --quiet 2>$null
+ git commit --quiet -m "Initial commit"
+ git push --quiet origin master 2>$null
+
+ # Remove symbolic-ref to force fallback
+ git remote set-head origin --delete 2>$null
+
+ # Create feature branch
+ git checkout --quiet -b test-branch
+ "test" | Set-Content "test-file.txt"
+ git add test-file.txt
+ git commit --quiet -m "Add test file"
+
+ $result = & pwsh -NoProfile -File $Script -Json 2>&1
+ $LASTEXITCODE | Should -Be 0
+ $json = $result | ConvertFrom-Json
+ $json.default_branch | Should -Be "master"
+ } finally {
+ Pop-Location
+ Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue
+ }
+ }
+ }
+
+ # ββββββββββββββββββββββββββββββββββββββββββββββ
+ # JSON Output Validation
+ # ββββββββββββββββββββββββββββββββββββββββββββββ
+
+ Describe "JSON output" {
+ It "has all required keys" {
+ $tmp = New-TempDir
+ try {
+ Initialize-GitRepo -Dir $tmp
+ Push-Location $tmp
+ "content" | Set-Content "new.txt"
+ git add new.txt
+
+ $result = & pwsh -NoProfile -File $Script -Json 2>&1
+ $LASTEXITCODE | Should -Be 0
+ $json = $result | ConvertFrom-Json
+ $json.PSObject.Properties.Name | Should -Contain "branch"
+ $json.PSObject.Properties.Name | Should -Contain "default_branch"
+ $json.PSObject.Properties.Name | Should -Contain "mode"
+ $json.PSObject.Properties.Name | Should -Contain "changed_files"
+ } finally {
+ Pop-Location
+ Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue
+ }
+ }
+ }
+
+ # ββββββββββββββββββββββββββββββββββββββββββββββ
+ # Edge Cases
+ # ββββββββββββββββββββββββββββββββββββββββββββββ
+
+ Describe "Edge cases" {
+ It "handles files with spaces in names" {
+ $tmp = New-TempDir
+ try {
+ Initialize-GitRepo -Dir $tmp
+ Push-Location $tmp
+ "content" | Set-Content "file with spaces.txt"
+ git add "file with spaces.txt"
+
+ $result = & pwsh -NoProfile -File $Script 2>&1
+ $LASTEXITCODE | Should -Be 0
+ ($result -join "`n") | Should -Match "file with spaces.txt"
+ } finally {
+ Pop-Location
+ Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue
+ }
+ }
+
+ It "handles nested directory changes" {
+ $tmp = New-TempDir
+ try {
+ Initialize-GitRepo -Dir $tmp
+ Push-Location $tmp
+ $nested = Join-Path $tmp "deep" "nested" "path"
+ New-Item -ItemType Directory -Path $nested -Force | Out-Null
+ "deep" | Set-Content (Join-Path $nested "file.txt")
+ git add .
+
+ $result = & pwsh -NoProfile -File $Script -Json 2>&1
+ $LASTEXITCODE | Should -Be 0
+ $json = $result | ConvertFrom-Json
+ $json.changed_files | Should -Contain "deep/nested/path/file.txt"
+ } finally {
+ Pop-Location
+ Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue
+ }
+ }
+
+ It "only reports ACMR files" {
+ $tmp = New-TempDir
+ try {
+ Initialize-GitRepo -Dir $tmp
+ Push-Location $tmp
+ "keep" | Set-Content "keep.txt"
+ "remove" | Set-Content "remove.txt"
+ git add .
+ git commit --quiet -m "Add files"
+
+ git rm --quiet remove.txt
+ "modified" | Set-Content "keep.txt"
+ "added" | Set-Content "added.txt"
+ git add .
+
+ $result = & pwsh -NoProfile -File $Script -Json 2>&1
+ $LASTEXITCODE | Should -Be 0
+ $json = $result | ConvertFrom-Json
+ $json.changed_files | Should -Contain "keep.txt"
+ $json.changed_files | Should -Contain "added.txt"
+ $json.changed_files | Should -Not -Contain "remove.txt"
+ } finally {
+ Pop-Location
+ Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue
+ }
+ }
+ }
+
+ # ββββββββββββββββββββββββββββββββββββββββββββββ
+ # Detached HEAD
+ # ββββββββββββββββββββββββββββββββββββββββββββββ
+
+ Describe "Detached HEAD" {
+ It "falls back to Mode B on detached HEAD" {
+ $tmp = New-TempDir
+ try {
+ Initialize-GitRepo -Dir $tmp
+ Push-Location $tmp
+ "file" | Set-Content "detached.txt"
+ git add detached.txt
+ git commit --quiet -m "Add file"
+ $commitHash = git rev-parse HEAD
+ git checkout --quiet $commitHash
+
+ "modified" | Set-Content "detached.txt"
+
+ $result = & pwsh -NoProfile -File $Script 2>&1
+ $LASTEXITCODE | Should -Be 0
+ ($result -join "`n") | Should -Match "Working directory changes"
+ ($result -join "`n") | Should -Match "detached.txt"
+ } finally {
+ Pop-Location
+ Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue
+ }
+ }
+ }
+
+ # ββββββββββββββββββββββββββββββββββββββββββββββ
+ # Text Output Format Validation
+ # ββββββββββββββββββββββββββββββββββββββββββββββ
+
+ Describe "Text output format" {
+ It "text mode output has correct format" {
+ $tmp = New-TempDir
+ try {
+ Initialize-GitRepo -Dir $tmp
+ Push-Location $tmp
+ "content" | Set-Content "formatted.txt"
+ git add formatted.txt
+
+ $result = & pwsh -NoProfile -File $Script 2>&1
+ $LASTEXITCODE | Should -Be 0
+ $text = $result -join "`n"
+ $text | Should -Match "BRANCH:"
+ $text | Should -Match "DEFAULT_BRANCH:"
+ $text | Should -Match "MODE:"
+ $text | Should -Match "CHANGED_FILES:"
+ $text | Should -Match "formatted.txt"
+ } finally {
+ Pop-Location
+ Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue
+ }
+ }
+ }
+
+ # ββββββββββββββββββββββββββββββββββββββββββββββ
+ # Branch Field Validation
+ # ββββββββββββββββββββββββββββββββββββββββββββββ
+
+ Describe "Branch field" {
+ It "branch field matches current branch name in Mode A" {
+ $tmp = New-TempDir
+ try {
+ Initialize-GitRepoWithRemote -Dir $tmp
+ Push-Location $tmp
+ git checkout --quiet -b my-feature-123
+ "x" | Set-Content "x.txt"
+ git add x.txt
+ git commit --quiet -m "commit"
+
+ $result = & pwsh -NoProfile -File $Script -Json 2>&1
+ $LASTEXITCODE | Should -Be 0
+ $json = $result | ConvertFrom-Json
+ $json.branch | Should -Be "my-feature-123"
+ } finally {
+ Pop-Location
+ Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue
+ }
+ }
+
+ It "branch field shows current branch on default branch (Mode B)" {
+ $tmp = New-TempDir
+ try {
+ Initialize-GitRepo -Dir $tmp
+ Push-Location $tmp
+ "change" | Set-Content "file.txt"
+ git add file.txt
+
+ $result = & pwsh -NoProfile -File $Script -Json 2>&1
+ $LASTEXITCODE | Should -Be 0
+ $json = $result | ConvertFrom-Json
+ $json.branch | Should -Be "main"
+ } finally {
+ Pop-Location
+ Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue
+ }
+ }
+ }
+
+ # ββββββββββββββββββββββββββββββββββββββββββββββ
+ # Special Characters in Filenames
+ # ββββββββββββββββββββββββββββββββββββββββββββββ
+
+ Describe "Special character filenames" {
+ It "handles filenames with special characters in JSON mode" {
+ $tmp = New-TempDir
+ try {
+ Initialize-GitRepo -Dir $tmp
+ Push-Location $tmp
+
+ # Create files with special characters
+ "a" | Set-Content "file (1).txt"
+ "b" | Set-Content "file's.txt"
+ "c" | Set-Content "file&more.txt"
+ git add .
+
+ $result = & pwsh -NoProfile -File $Script -Json 2>&1
+ $LASTEXITCODE | Should -Be 0
+ $json = $result | ConvertFrom-Json
+ $json.changed_files | Should -Contain "file (1).txt"
+ $json.changed_files | Should -Contain "file's.txt"
+ $json.changed_files | Should -Contain "file&more.txt"
+ } finally {
+ Pop-Location
+ Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue
+ }
+ }
+
+ It "handles filenames with double quotes in JSON mode" -Skip:($IsWindows -or $env:OS -eq 'Windows_NT') {
+ $tmp = New-TempDir
+ try {
+ Initialize-GitRepo -Dir $tmp
+ Push-Location $tmp
+
+ # Create a file with a double quote in its name
+ # (skipped on Windows β NTFS does not allow " in filenames)
+ $fname = 'file"quote.txt'
+ [System.IO.File]::WriteAllText((Join-Path $tmp $fname), "content")
+ git add .
+
+ $result = & pwsh -NoProfile -File $Script -Json 2>&1
+ $LASTEXITCODE | Should -Be 0
+ $json = $result | ConvertFrom-Json
+ $json.changed_files.Count | Should -Be 1
+ $json.changed_files[0] | Should -Match 'quote'
+ } finally {
+ Pop-Location
+ Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue
+ }
+ }
+ }
+}
diff --git a/.specify/feature.json b/.specify/feature.json
index 7a8376e29b..cfeffb4888 100644
--- a/.specify/feature.json
+++ b/.specify/feature.json
@@ -1,4 +1 @@
-{
- "feature_directory": "specs/002-node-list-layout"
-}
-
+{"feature_directory":"specs/018-compose-screenshot-testing"}
diff --git a/.specify/memory/constitution.md b/.specify/memory/constitution.md
index 78f26df645..71f7725895 100644
--- a/.specify/memory/constitution.md
+++ b/.specify/memory/constitution.md
@@ -1,20 +1,15 @@
<!--
SYNC IMPACT REPORT
==================
-Version change: 1.2.2 β 1.2.3
+Version change: 1.1.0 β 1.1.1
Modified principles:
- - Principle I: "where feasible" β "unless the platform API has no KMP
- equivalent" (interpretability fix)
- - Principle V: "significant UI changes" β "new screens, layout
- restructuring, or navigation changes" (interpretability fix)
- - Principle IX: "~5 logical commits" β "5 logical commits" (precision)
-Compressed sections:
- - Development Workflow: replaced verbose procedural content with
- 3-line summary referencing AGENTS.md and skills
- - Architecture Constraints: replaced verbose tech stack listings with
- constraint summaries referencing kmp-architecture SKILL
-Removed sections: None (content preserved in AGENTS.md and skills)
-Templates requiring updates: None.
+ - Governance (compliance review wording clarified to require plan-level constitution checks)
+Added sections: None.
+Removed sections: None.
+Templates requiring updates:
+ β
.specify/templates/plan-template.md β Constitution Check now enumerates the six project principles.
+ β
.specify/templates/spec-template.md β Validated against current constitution; no template changes required.
+ β
.specify/templates/tasks-template.md β Added required verification and design review task guidance.
Follow-up TODOs: None.
-->
@@ -24,67 +19,53 @@ Follow-up TODOs: None.
### I. Kotlin Multiplatform Core
-Business logic MUST reside exclusively in `commonMain` source sets.
-KMP-equivalent libraries MUST be used in place of JVM/Android-specific
-APIs:
+Business logic MUST reside exclusively in `commonMain` source sets. KMP-equivalent libraries
+MUST be used in place of JVM/Android-specific APIs:
-- MUST use Okio (not `java.io`), Ktor (not `java.net`/OkHttp in common),
- Mutex/atomicfu (not `java.util.concurrent`), Room KMP, DataStore KMP,
- and Koin 4.2+ with K2 Compiler Plugin.
+- MUST use Okio (not `java.io`), Ktor (not `java.net`/OkHttp in common), Mutex/atomicfu
+ (not `java.util.concurrent`), Room KMP, DataStore KMP, and Koin 4.2+.
- MUST NOT import `java.*` or `android.*` in any `commonMain` module.
-- Platform-specific implementations belong in `androidMain`/`desktopMain`
- actual declarations only.
-- Platform capabilities MUST prefer interface + DI over `expect`/`actual`
- unless the platform API has no KMP equivalent.
-- Rationale: The project goal is multi-platform parity (Android, Desktop,
- iOS). Framework bleed in `commonMain` breaks compilability on non-Android
- targets and undermines the entire decoupling effort.
+- Platform-specific implementations belong in `androidMain`/`desktopMain` actual
+ declarations only.
+- Rationale: The project goal is multi-platform parity (Android, Desktop, iOS). Framework
+ bleed in `commonMain` breaks compilability on non-Android targets and undermines the
+ entire decoupling effort.
### II. Zero Lint Tolerance
All code contributions MUST pass static analysis before merge:
-- `./gradlew spotlessApply` MUST be run and `spotlessCheck` MUST pass with
- no violations.
+- `./gradlew spotlessApply` MUST be run and `spotlessCheck` MUST pass with no violations.
- `detekt` MUST pass with no new violations introduced.
- A task or PR is considered incomplete if either check fails.
-- Rationale: Consistent code style and static analysis gates prevent
- technical debt accumulation and catch bugs that tests alone miss.
+- Rationale: Consistent code style and static analysis gates prevent technical debt
+ accumulation and catch bugs that tests alone miss.
### III. Compose Multiplatform UI
-All UI MUST use JetBrains Compose Multiplatform, not Android-only Jetpack
-Compose APIs:
-
-- MUST use `MeshtasticNavDisplay` and `NavigationBackHandler` for
- navigation across all entry points (not Android's `BackHandler`).
-- Navigation routes MUST be `@Serializable sealed interface` types defined
- in `core:navigation`.
-- Feature navigation graphs MUST be extension functions on
- `EntryProviderScope<NavKey>` in `commonMain`.
-- Floats MUST be pre-formatted using `NumberFormatter.format()` before
- display β CMP only supports `%N$s` (string) and `%N$d` (int) format
- specifiers.
-- UI MUST compile and render correctly on all supported targets (Android,
- Compose Desktop).
-- Material 3 Adaptive MUST be used for responsive layouts.
-- Rationale: Compose Multiplatform ensures UI consistency across platforms
- and enforces the project's multi-platform architecture goal.
+All UI MUST use JetBrains Compose Multiplatform, not Android-only Jetpack Compose APIs:
+
+- MUST use `MeshtasticNavDisplay` and `NavigationBackHandler` for navigation across all
+ entry points.
+- Floats MUST be pre-formatted using `NumberFormatter.format()` before display in any
+ composable.
+- UI MUST compile and render correctly on all supported targets (Android, Compose Desktop).
+- Rationale: Compose Multiplatform ensures UI consistency across platforms and enforces the
+ project's multi-platform architecture goal.
### IV. Privacy First
-The application handles sensitive mesh network data; user privacy MUST be
-protected at all times:
+The application handles sensitive mesh network data; user privacy MUST be protected at all
+times:
-- MUST NOT log or expose PII, location data, or cryptographic keys in
- logs, crash reports, or any debug output.
-- Secrets MUST be git-ignored and MUST NOT be committed to the repository
- under any circumstances.
+- MUST NOT log or expose PII, location data, or cryptographic keys in logs, crash reports,
+ or any debug output.
+- Secrets MUST be git-ignored and MUST NOT be committed to the repository under any
+ circumstances.
- `core/proto` is a read-only upstream submodule (`meshtastic/protobufs`). MUST NOT modify
`.proto` files directly; proto changes require an upstream issue labeled `upstream`.
-- Rationale: Meshtastic users rely on the mesh for private, off-grid
- communications. Data leaks could endanger users in sensitive or
- adversarial deployments.
+- Rationale: Meshtastic users rely on the mesh for private, off-grid communications. Data
+ leaks could endanger users in sensitive or adversarial deployments.
### V. Design Standards Compliance
@@ -92,139 +73,82 @@ All user-facing UI MUST conform to the Meshtastic Client Design Standards:
- The canonical reference lives at:
`https://raw.githubusercontent.com/meshtastic/design/refs/heads/master/standards/meshtastic_design_standards_latest.md`
-- New screens, layout restructuring, or navigation changes MUST be
- reviewed against the design standards before merge.
-- Deviations from the design standards require explicit justification in
- the PR description with a rationale for why the standard cannot or
- should not be followed.
-- Rationale: Consistent cross-platform UX across Android, iOS, and other
- clients ensures users have a predictable experience regardless of
- platform. The design standards are maintained collaboratively across
- all Meshtastic client teams.
+- New screens and significant UI changes MUST be reviewed against the design standards
+ before merge.
+- Deviations from the design standards require explicit justification in the PR description
+ with a rationale for why the standard cannot or should not be followed.
+- Rationale: Consistent cross-platform UX across Android, iOS, and other clients ensures
+ users have a predictable experience regardless of platform. The design standards are
+ maintained collaboratively across all Meshtastic client teams.
### VI. Verify Before Push
Local verification MUST complete successfully before any `git push`:
-- MUST run the full verification command:
- `./gradlew spotlessApply spotlessCheck detekt assembleDebug test allTests`
-- Both `test` AND `allTests` are required: `allTests` covers KMP modules;
- `test` covers pure-Android modules. Neither alone catches everything.
+- MUST run `./gradlew spotlessApply spotlessCheck detekt` plus relevant module `:test`
+ tasks for all modules touched.
- After pushing, CI status MUST be confirmed via `gh pr checks <PR>` or
- `gh run list --branch <branch> --limit 5`. Phrases like "CI should be
- green" are explicitly prohibited β check and confirm.
-- Rationale: CI has failed repeatedly due to skipped local checks.
- Verification is a hard gate, not an optimistic assumption.
-
-### VII. Coroutine Safety
-
-Coroutine code MUST use project-standard utilities that preserve
-structured concurrency and cancellation semantics:
-
-- MUST use `safeCatching {}` (from `core:common`) instead of
- `runCatching {}` in suspend/coroutine code β `runCatching` silently
- swallows `CancellationException`, breaking structured concurrency.
-- MUST use `org.meshtastic.core.common.util.ioDispatcher` β never
- `Dispatchers.IO` directly. Inject `CoroutineDispatchers` from
- `core:di` for testability.
-- Rationale: Incorrect exception handling in coroutines causes silent
- failures and resource leaks. Centralizing dispatcher injection enables
- deterministic testing.
-
-### VIII. Resource Discipline
-
-All user-visible text, icons, and formatting MUST use project-standard
-resource APIs:
-
-- All strings MUST reside in
- `core/resources/src/commonMain/composeResources/values/strings.xml`.
- Use `stringResource(Res.string.key)` β never hardcoded strings in UI.
-- After adding any string resource, MUST run
- `python3 scripts/sort-strings.py` to maintain alphabetical order and
- regenerate `strings-index.txt`.
-- Consult `strings-index.txt` before reading large string files to
- minimize context waste.
-- MUST use `MeshtasticIcons` (from `core/ui/icon/`) instead of
- `material.icons.Icons` for all iconography.
-- Rationale: Centralized resources enable localization via Crowdin,
- maintain consistency with the Meshtastic design language, and prevent
- scattered hardcoded strings that resist translation.
-
-### IX. Branch & Scope Hygiene
-
-All branches and PRs MUST follow naming conventions and scope discipline:
-
-- Branch names MUST start with one of: `feat/`, `fix/`, `chore/`, `docs/`,
- `build/`, `ci/`, `refactor/`, `test/`, `deps/`, or a numeric spec prefix
- (e.g., `002-feature-name` for spec-driven feature branches created by
- Spec Kit).
-- Branches MUST be created off fetched upstream: `git fetch origin &&
- git checkout -b <name> origin/main`. Never branch from a personal
- fork's `main` β it may be stale.
-- When a working branch grows beyond 5 logical commits or spans
- unrelated concerns, contributors MUST propose a fresh branch off
- `origin/main`, cherry-pick high-impact changes, and defer tangential
- work to follow-up PRs.
-- Fixup commits MUST be squashed before pushing.
-- Rationale: Focused branches reduce review burden, minimize merge
- conflicts, and maintain a clean git history. Upstream-based branching
- prevents stale-fork drift.
+ `gh run list --branch <branch> --limit 5`. Phrases like "CI should be green" are
+ explicitly prohibited.
+- Rationale: CI has failed repeatedly due to skipped local checks. Verification is a hard
+ gate, not an optimistic assumption.
## Development Workflow
-Follow the bootstrap, verification, and workflow procedures defined in
-AGENTS.md `<process_essentials>` and the relevant `.skills/` playbooks
-(`project-overview`, `testing-ci`, `new-branch`, `code-review`). All
-workflow steps there are non-negotiable.
+The following workflow steps are non-negotiable for all contributors and agents:
-Key constraints reiterated here for governance compliance:
-- Baseline verification (`spotlessApply spotlessCheck detekt assembleDebug
- test allTests`) MUST pass before any push or PR.
-- Gradle task naming differs between KMP and Android-only modules β see
- `.github/copilot-instructions.md` Β§Gradle task naming for the
- authoritative table.
-- Two app flavors (`fdroid` / `google`) use different signing keys. Only
- one can be installed at a time β uninstall before switching.
+- **Bootstrap First**: The mandatory bootstrap steps in `.skills/project-overview/SKILL.md`
+ MUST be executed before any build operation in a new session.
+- **Baseline Verification**: Before any PR is opened or pushed, run:
+ `./gradlew spotlessApply spotlessCheck detekt assembleDebug test allTests`
+- **String Resources**: After adding any string resource, run
+ `python3 scripts/sort-strings.py` to maintain alphabetical organization and regenerate
+ `strings-index.txt`. Consult `strings-index.txt` before reading large string files.
+- **Memory Persistence**: `.agent_memory/session_context.md` MUST be updated at the end of
+ every agent session or major task to preserve context across sessions.
+- **Plan Before Execution**: Complex refactors MUST have a plan written in `.agent_plans/`
+ (git-ignored) before execution begins.
+- **Context Discipline**: Agents MUST NOT read binary files (PNG, MP3, etc.) or vacuum the
+ entire codebase for localized fixes. Limit context reads to relevant modules.
## Architecture Constraints
-Module boundaries and technology choices are fixed. See AGENTS.md
-`<context_and_memory>` and `.skills/kmp-architecture/SKILL.md` for the
-full stack and module map.
-
-Key constraints:
-- No alternative DI, networking, or BLE frameworks may be introduced.
-- Java source files MUST NOT be introduced in KMP modules.
-- No reactive frameworks other than Coroutines/Flow in `commonMain`.
-- All navigation MUST use `MeshtasticNavDisplay`.
-- Gradle Kotlin DSL with convention plugins in `build-logic/`. Two
- flavors: `fdroid` (OSS) / `google` (Maps + DataDog).
+The following module boundaries and technology choices are fixed for this project:
+
+- **KMP Modules**: `core:domain` (business logic), `core:data` (repositories),
+ `core:database` (Room KMP), `core:datastore` (preferences), `core:network` (Ktor),
+ `core:ble` (Kable multiplatform BLE).
+- **State Management**: Unidirectional Data Flow (UDF) with ViewModels, Kotlin Coroutines,
+ and Flow. No reactive frameworks other than Coroutines/Flow in `commonMain`.
+- **Dependency Injection**: Koin 4.2+ with Koin Annotations and the K2 Compiler Plugin.
+ No alternative DI framework may be introduced.
+- **Navigation**: JetBrains Navigation 3 for multiplatform routing with RESTful deep
+ linking. All navigation MUST use `MeshtasticNavDisplay`.
+- **Data Protocol**: Protobuf for device communications (read-only upstream submodule).
+ Room KMP for local persistence. DataStore for user preferences.
+- **Language & Toolchain**: Kotlin 2.3+ targeting JDK 21. Java source files MUST NOT be
+ introduced in KMP modules.
## Governance
-This constitution supersedes all other practices, coding guidelines, and
-agent instructions. `AGENTS.md` is the authoritative source of truth. The
-files `.github/copilot-instructions.md`, `CLAUDE.md`, and `GEMINI.md`
-MUST redirect to `AGENTS.md` and MUST NOT diverge from it.
+This constitution supersedes all other practices, coding guidelines, and agent instructions.
+`AGENTS.md` is the authoritative source of truth. The files
+`.github/copilot-instructions.md`, `CLAUDE.md`, and `GEMINI.md` MUST redirect to
+`AGENTS.md` and MUST NOT diverge from it.
**Amendment Procedure**:
-1. Propose the amendment with rationale and a migration plan in a PR
- description.
-2. Update `AGENTS.md` and this constitution atomically in the same
- commit.
+1. Propose the amendment with rationale and a migration plan in a PR description.
+2. Update `AGENTS.md` and this constitution atomically in the same commit.
3. Increment `CONSTITUTION_VERSION` per the versioning policy below.
-4. All PRs and code reviews MUST verify compliance with the current
- constitution version.
+4. All PRs and code reviews MUST verify compliance with the current constitution version.
**Versioning Policy**:
-- MAJOR: Backward-incompatible principle removal or fundamental
- redefinition.
+- MAJOR: Backward-incompatible principle removal or fundamental redefinition.
- MINOR: New principle or section added, or materially expanded guidance.
- PATCH: Clarifications, wording fixes, or non-semantic refinements.
-**Compliance Review**: Every PR description MUST include a Constitution
-Check confirming all nine principles were evaluated. Complexity violations
-require explicit justification in the Complexity Tracking table of the
-plan document.
+**Compliance Review**: Every implementation plan and PR description MUST include a
+Constitution Check confirming all six principles were evaluated. Complexity violations
+require explicit justification in the Complexity Tracking table of the plan document.
-**Version**: 1.2.3 | **Ratified**: 2026-05-07 | **Last Amended**: 2026-05-09
+**Version**: 1.1.1 | **Ratified**: 2026-05-07 | **Last Amended**: 2026-05-08
diff --git a/.specify/templates/plan-template.md b/.specify/templates/plan-template.md
index b37ce284c7..eab901eaaf 100644
--- a/.specify/templates/plan-template.md
+++ b/.specify/templates/plan-template.md
@@ -13,37 +13,41 @@
<!--
ACTION REQUIRED: Replace the content in this section with the technical details
- for the feature. Most fields below are pre-filled with project defaults β
- adjust only what's feature-specific.
+ for the project. The structure here is presented in advisory capacity to guide
+ the iteration process.
-->
-**Language/Version**: Kotlin 2.3+ targeting JDK 21
-**Primary Dependencies**: Compose Multiplatform, Material 3 Adaptive, Koin 4.2+ (K2 Compiler Plugin), Room KMP, DataStore KMP
-**Storage**: [DataStore KMP for preferences / Room KMP for entities / N/A]
-**Testing**: KMP `allTests` for `feature:*` and `core:*` modules; `testFdroidDebugUnitTest` for `app`
-**Target Platform**: Android, Desktop (JVM), iOS β all via `commonMain`
-**Project Type**: Mobile/desktop app (Kotlin Multiplatform)
-**Performance Goals**: [e.g., 60fps scrolling, <1s response or NEEDS CLARIFICATION]
-**Constraints**: All UI in `commonMain`; no `java.*`/`android.*` in common; CMP float pre-formatting via `NumberFormatter.format()`
-**Scale/Scope**: [e.g., N new files, M modified files across feature/X, core/Y]
+**Language/Version**: [e.g., Python 3.11, Swift 5.9, Rust 1.75 or NEEDS CLARIFICATION]
+**Primary Dependencies**: [e.g., FastAPI, UIKit, LLVM or NEEDS CLARIFICATION]
+**Storage**: [if applicable, e.g., PostgreSQL, CoreData, files or N/A]
+**Testing**: [e.g., pytest, XCTest, cargo test or NEEDS CLARIFICATION]
+**Target Platform**: [e.g., Linux server, iOS 15+, WASM or NEEDS CLARIFICATION]
+**Project Type**: [e.g., library/cli/web-service/mobile-app/compiler/desktop-app or NEEDS CLARIFICATION]
+**Performance Goals**: [domain-specific, e.g., 1000 req/s, 10k lines/sec, 60 fps or NEEDS CLARIFICATION]
+**Constraints**: [domain-specific, e.g., <200ms p95, <100MB memory, offline-capable or NEEDS CLARIFICATION]
+**Scale/Scope**: [domain-specific, e.g., 10k users, 1M LOC, 50 screens or NEEDS CLARIFICATION]
## Constitution Check
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
-| Principle | Status | Notes |
-|-----------|--------|-------|
-| I. Kotlin Multiplatform Core | β¬ | All code in `commonMain`. No `java.*`/`android.*` imports. |
-| II. Zero Lint Tolerance | β¬ | `spotlessApply` + `detekt` required before merge. |
-| III. Compose Multiplatform UI | β¬ | CMP composables, `NumberFormatter.format()` for floats, Navigation 3 patterns. |
-| IV. Privacy First | β¬ | No PII/location/key logging. Proto submodule read-only. |
-| V. Design Standards Compliance | β¬ | UI-GATE review required before UI work. |
-| VI. Verify Before Push | β¬ | Full verification: `./gradlew spotlessApply spotlessCheck detekt assembleDebug test allTests`. |
-| VII. Coroutine Safety | β¬ | `safeCatching {}` not `runCatching {}`. Project `ioDispatcher` not `Dispatchers.IO`. |
-| VIII. Resource Discipline | β¬ | `stringResource(Res.string.key)`, `MeshtasticIcons`, `sort-strings.py` after adding strings. |
-| IX. Branch & Scope Hygiene | β¬ | Branch prefix, upstream base, ~5-commit scope limit. |
-
-**Gate Result**: [β¬ Pending / β
All principles satisfied / β Violations requiring justification]
+- **I. Kotlin Multiplatform Core**: Identify every touched source set/module and confirm
+ all business logic remains in `commonMain`; document any platform-specific work that is
+ isolated to `androidMain`/platform shells.
+- **II. Zero Lint Tolerance**: List the exact formatting and static-analysis commands that
+ will be run for the touched modules, at minimum `spotlessCheck` and `detekt`.
+- **III. Compose Multiplatform UI**: If UI is in scope, confirm the design uses Compose
+ Multiplatform patterns, `MeshtasticNavDisplay`/`NavigationBackHandler` where relevant,
+ and pre-formats floats with `NumberFormatter.format()`.
+- **IV. Privacy First**: Confirm the change does not log or expose PII, location data,
+ cryptographic keys, or modify the read-only `core/proto` submodule.
+- **V. Design Standards Compliance**: For any user-facing UI, record how the design was
+ checked against the Meshtastic Client Design Standards, or explicitly mark the gate N/A.
+- **VI. Verify Before Push**: Record the exact local verification commands and the expected
+ post-push CI check command (`gh pr checks` or `gh run list`) before implementation starts.
+
+If any gate cannot be met, the exception MUST be justified in the Complexity Tracking
+section below and explicitly called out in the PR description.
## Project Structure
@@ -60,91 +64,51 @@ specs/[###-feature]/
```
### Source Code (repository root)
-
<!--
- ACTION REQUIRED: Fill in with the actual files affected by this feature.
- Use the module layout below as a guide. Delete unused modules.
+ ACTION REQUIRED: Replace the placeholder tree below with the concrete layout
+ for this feature. Delete unused options and expand the chosen structure with
+ real paths (e.g., apps/admin, packages/something). The delivered plan must
+ not include Option labels.
-->
```text
-feature/[name]/ β Primary changes
-βββ src/commonMain/kotlin/org/meshtastic/feature/[name]/
-β βββ component/
-β β βββ [ExistingComposable].kt β Modify
-β β βββ [NewComposable].kt β NEW
-β βββ list/
-β β βββ [Screen].kt β Modify β [description]
-β β βββ [ViewModel].kt β Modify β [description]
-β βββ model/
-β βββ [NewModel].kt β NEW β [description]
-
-core/[module]/ β Core layer changes
-βββ src/commonMain/kotlin/org/meshtastic/core/[module]/
-β βββ [File].kt β Modify β [description]
-
-feature/settings/ β Settings integration (if applicable)
-βββ src/commonMain/kotlin/org/meshtastic/feature/settings/
-β βββ [SettingsSection].kt β NEW β [description]
-
-core/resources/
-βββ src/commonMain/composeResources/values/strings.xml β Add string resources
+# [REMOVE IF UNUSED] Option 1: Single project (DEFAULT)
+src/
+βββ models/
+βββ services/
+βββ cli/
+βββ lib/
+
+tests/
+βββ contract/
+βββ integration/
+βββ unit/
+
+# [REMOVE IF UNUSED] Option 2: Web application (when "frontend" + "backend" detected)
+backend/
+βββ src/
+β βββ models/
+β βββ services/
+β βββ api/
+βββ tests/
+
+frontend/
+βββ src/
+β βββ components/
+β βββ pages/
+β βββ services/
+βββ tests/
+
+# [REMOVE IF UNUSED] Option 3: Mobile + API (when "iOS/Android" detected)
+api/
+βββ [same as backend above]
+
+ios/ or android/
+βββ [platform-specific structure: feature modules, UI flows, platform tests]
```
-**Structure Decision**: [Document the selected structure and why existing modules
-are modified rather than creating new ones, per KMP module architecture.]
-
-## Module Impact
-
-| Module | Change Type | Files Affected | Risk |
-|--------|-------------|----------------|------|
-| `feature/[name]` | New + Modify | [count] | [Low/Medium/High] |
-| `core/[module]` | Modify | [count] | [Low/Medium/High] |
-| `core/resources` | Modify | 1 file (strings.xml) | Low |
-
-## Integration Points
-
-<!--
- Document how this feature integrates with existing systems:
- navigation routes, DataStore keys, DI modules, etc.
--->
-
-## Design Constraints
-
-<!--
- List technical constraints specific to this feature.
- Include M3/Expressive, accessibility, and CMP constraints.
--->
-
-- All UI lives in `commonMain` β not platform-specific
-- Strings accessed via `stringResource(Res.string.key)` β never hardcoded
-- Icons use `MeshtasticIcons` exclusively (from `core/ui/icon/`)
-- Error handling uses `safeCatching {}` not `runCatching {}`
-- Dispatchers via `org.meshtastic.core.common.util.ioDispatcher`
-- Float values must be pre-formatted with `NumberFormatter.format()` (CMP constraint)
-
-## Risk Assessment
-
-| Risk | Likelihood | Impact | Mitigation |
-|------|-----------|--------|------------|
-| [Risk description] | [Low/Med/High] | [Low/Med/High] | [Mitigation with task reference] |
-
-## Phase Alignment with Tasks
-
-<!--
- ACTION REQUIRED: Fill in after tasks.md is generated by /speckit.tasks.
- Reference actual task IDs from tasks.md β do NOT use plan-internal numbering.
--->
-
-| Phase | Purpose | Key Tasks | Dependencies |
-|-------|---------|-----------|--------------|
-| 1. Setup | [Purpose] | [Task IDs] | None |
-| N. Polish | [Purpose] | [Task IDs] | All prior phases |
-
-### Critical Path
-
-```
-Phase 1 β Phase 2 β ... β Phase N
-```
+**Structure Decision**: [Document the selected structure and reference the real
+directories captured above]
## Complexity Tracking
@@ -152,4 +116,5 @@ Phase 1 β Phase 2 β ... β Phase N
| Violation | Why Needed | Simpler Alternative Rejected Because |
|-----------|------------|-------------------------------------|
-| *None* | β | β |
+| [e.g., 4th project] | [current need] | [why 3 projects insufficient] |
+| [e.g., Repository pattern] | [specific problem] | [why direct DB access insufficient] |
diff --git a/.specify/templates/tasks-template.md b/.specify/templates/tasks-template.md
index cd465c3cba..927cb4e5aa 100644
--- a/.specify/templates/tasks-template.md
+++ b/.specify/templates/tasks-template.md
@@ -8,11 +8,17 @@ description: "Task list template for feature implementation"
**Input**: Design documents from `/specs/[###-feature-name]/`
**Prerequisites**: plan.md (required), spec.md (required for user stories), research.md, data-model.md, contracts/
-**Tests**: The examples below include test tasks. Tests are OPTIONAL - only include them if explicitly requested in the feature specification.
+**Tests**: The examples below include test tasks. New automated tests are OPTIONAL and
+should only be included when requested by the feature specification or when needed to
+verify the implementation safely.
+
+**Verification**: Every generated task list MUST include constitution-required validation
+tasks for formatting, static analysis, and the relevant compile/test commands for the
+touched modules before work is considered complete.
**Organization**: Tasks are grouped by user story to enable independent implementation and testing of each story.
-## Format: `[ID] [P?] [Story?] Description`
+## Format: `[ID] [P?] [Story] Description`
- **[P]**: Can run in parallel (different files, no dependencies)
- **[Story]**: Which user story this task belongs to (e.g., US1, US2, US3)
@@ -20,13 +26,10 @@ description: "Task list template for feature implementation"
## Path Conventions
-- **KMP commonMain**: `feature/[name]/src/commonMain/kotlin/org/meshtastic/feature/[name]/`
-- **Core modules**: `core/[module]/src/commonMain/kotlin/org/meshtastic/core/[module]/`
-- **Core UI**: `core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/`
-- **Settings**: `feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/`
-- **Resources**: `core/resources/src/commonMain/composeResources/values/strings.xml`
-- **Tests (KMP)**: `feature/[name]/src/commonTest/kotlin/`
-- **Tests (Android-only)**: `app/src/test/`
+- **Single project**: `src/`, `tests/` at repository root
+- **Web app**: `backend/src/`, `frontend/src/`
+- **Mobile**: `api/src/`, `ios/src/` or `android/src/`
+- Paths shown below assume single project - adjust based on plan.md structure
<!--
============================================================================
@@ -49,27 +52,30 @@ description: "Task list template for feature implementation"
## Phase 1: Setup (Shared Infrastructure)
-**Purpose**: Design gate, enums, DataStore/Room keys, and string resources required by all user stories.
-
-- [ ] T001 `[UI-GATE]` Review `.skills/design-standards/SKILL.md` and upstream Meshtastic design standards; record constraints for new composables. This task blocks all UI work.
-- [ ] T002 [P] Create model/enum files in `feature/[name]/src/commonMain/.../model/`
-- [ ] T003 [P] Add DataStore/Room preference keys in `core/prefs/` or `core/datastore/`
-- [ ] T004 Add string resources to `core/resources/src/commonMain/composeResources/values/strings.xml`. Run `python3 scripts/sort-strings.py` after.
+**Purpose**: Project initialization and basic structure
-**Dependencies**: T001 blocks all UI phases. T002 and T003 are independent.
-**Checkpoint**: Preference infrastructure ready β all user stories can now begin.
+- [ ] T001 Create project structure per implementation plan
+- [ ] T002 Initialize [language] project with [framework] dependencies
+- [ ] T003 [P] Configure linting and formatting tools
---
## Phase 2: Foundational (Blocking Prerequisites)
-**Purpose**: Core infrastructure, accessibility fixes, and ViewModel wiring that MUST complete before ANY user story can ship.
+**Purpose**: Core infrastructure that MUST be complete before ANY user story can be implemented
+
+**β οΈ CRITICAL**: No user story work can begin until this phase is complete
-- [ ] T005 [Required existing file changes β e.g., accessibility fixes, ViewModel wiring]
-- [ ] T006 [Modify ViewModel to expose new StateFlows from DataStore]
+Examples of foundational tasks (adjust based on your project):
-**Dependencies**: Phase 1 must complete first.
-**Checkpoint**: Foundation ready β user story implementation can begin.
+- [ ] T004 Setup database schema and migrations framework
+- [ ] T005 [P] Implement authentication/authorization framework
+- [ ] T006 [P] Setup API routing and middleware structure
+- [ ] T007 Create base models/entities that all stories depend on
+- [ ] T008 Configure error handling and logging infrastructure
+- [ ] T009 Setup environment configuration management
+
+**Checkpoint**: Foundation ready - user story implementation can now begin in parallel
---
@@ -79,13 +85,23 @@ description: "Task list template for feature implementation"
**Independent Test**: [How to verify this story works on its own]
+### Tests for User Story 1 (OPTIONAL - only if tests requested) β οΈ
+
+> **NOTE: Write these tests FIRST, ensure they FAIL before implementation**
+
+- [ ] T010 [P] [US1] Contract test for [endpoint] in tests/contract/test_[name].py
+- [ ] T011 [P] [US1] Integration test for [user journey] in tests/integration/test_[name].py
+
### Implementation for User Story 1
-- [ ] T010 [P] [US1] Create composable in `feature/[name]/src/commonMain/.../component/[Name].kt`
-- [ ] T011 [US1] Implement core UI logic (depends on T010)
-- [ ] T012 [US1] Wire into screen in `feature/[name]/src/commonMain/.../list/[Screen].kt`
+- [ ] T012 [P] [US1] Create [Entity1] model in src/models/[entity1].py
+- [ ] T013 [P] [US1] Create [Entity2] model in src/models/[entity2].py
+- [ ] T014 [US1] Implement [Service] in src/services/[service].py (depends on T012, T013)
+- [ ] T015 [US1] Implement [endpoint/feature] in src/[location]/[file].py
+- [ ] T016 [US1] Add validation and error handling
+- [ ] T017 [US1] Add logging for user story 1 operations
-**Checkpoint**: User Story 1 complete β [core feature] works end-to-end.
+**Checkpoint**: At this point, User Story 1 should be fully functional and testable independently
---
@@ -95,12 +111,40 @@ description: "Task list template for feature implementation"
**Independent Test**: [How to verify this story works on its own]
+### Tests for User Story 2 (OPTIONAL - only if tests requested) β οΈ
+
+- [ ] T018 [P] [US2] Contract test for [endpoint] in tests/contract/test_[name].py
+- [ ] T019 [P] [US2] Integration test for [user journey] in tests/integration/test_[name].py
+
### Implementation for User Story 2
-- [ ] T020 [US2] Implement [feature component]
-- [ ] T021 [US2] Add settings UI in `feature/settings/src/commonMain/.../[Name].kt`
+- [ ] T020 [P] [US2] Create [Entity] model in src/models/[entity].py
+- [ ] T021 [US2] Implement [Service] in src/services/[service].py
+- [ ] T022 [US2] Implement [endpoint/feature] in src/[location]/[file].py
+- [ ] T023 [US2] Integrate with User Story 1 components (if needed)
-**Checkpoint**: User Story 2 complete.
+**Checkpoint**: At this point, User Stories 1 AND 2 should both work independently
+
+---
+
+## Phase 5: User Story 3 - [Title] (Priority: P3)
+
+**Goal**: [Brief description of what this story delivers]
+
+**Independent Test**: [How to verify this story works on its own]
+
+### Tests for User Story 3 (OPTIONAL - only if tests requested) β οΈ
+
+- [ ] T024 [P] [US3] Contract test for [endpoint] in tests/contract/test_[name].py
+- [ ] T025 [P] [US3] Integration test for [user journey] in tests/integration/test_[name].py
+
+### Implementation for User Story 3
+
+- [ ] T026 [P] [US3] Create [Entity] model in src/models/[entity].py
+- [ ] T027 [US3] Implement [Service] in src/services/[service].py
+- [ ] T028 [US3] Implement [endpoint/feature] in src/[location]/[file].py
+
+**Checkpoint**: All user stories should now be independently functional
---
@@ -110,15 +154,15 @@ description: "Task list template for feature implementation"
## Phase N: Polish & Cross-Cutting Concerns
-**Purpose**: Performance validation, edge case hardening, tests, and verification.
+**Purpose**: Improvements that affect multiple user stories
-- [ ] TXXX [P] Verify performance targets (e.g., 60fps scrolling with N+ items)
-- [ ] TXXX [P] Ensure all float values use `NumberFormatter.format()` before display
-- [ ] TXXX Validate edge cases documented in spec
-- [ ] TXXX [P] Write unit tests in `feature/[name]/src/commonTest/`
-- [ ] TXXX [P] Write Compose UI tests in `feature/[name]/src/commonTest/`
-- [ ] TXXX Run `./gradlew :feature:[name]:allTests :core:[module]:allTests`
-- [ ] TXXX Run `./gradlew spotlessApply spotlessCheck detekt assembleDebug test allTests`
+- [ ] TXXX [P] Documentation updates in docs/
+- [ ] TXXX Code cleanup and refactoring
+- [ ] TXXX [P] Review user-facing UI against Meshtastic design standards and document any approved deviations
+- [ ] TXXX [P] Confirm no logs, telemetry, or config changes expose PII, location data, secrets, or modify `core/proto`
+- [ ] TXXX [P] Run constitution-required verification (`spotlessCheck`, `detekt`, and relevant compile/test tasks)
+- [ ] TXXX [P] Additional unit/integration tests (if requested) in the appropriate test source sets
+- [ ] TXXX Validate quickstart/README or developer workflow docs if contributor workflow changed
---
@@ -126,28 +170,48 @@ description: "Task list template for feature implementation"
### Phase Dependencies
-- **Setup (Phase 1)**: No dependencies β T001 UI-GATE blocks all UI phases
-- **Foundational (Phase 2)**: Depends on Phase 1 β BLOCKS all user stories
-- **User Stories (Phase 3+)**: Depend on Phase 2 completion
- - User stories proceed sequentially by priority (P1 β P2 β P3) unless independent
-- **Polish (Final Phase)**: Depends on all user story phases
+- **Setup (Phase 1)**: No dependencies - can start immediately
+- **Foundational (Phase 2)**: Depends on Setup completion - BLOCKS all user stories
+- **User Stories (Phase 3+)**: All depend on Foundational phase completion
+ - User stories can then proceed in parallel (if staffed)
+ - Or sequentially in priority order (P1 β P2 β P3)
+- **Polish (Final Phase)**: Depends on all desired user stories being complete
### User Story Dependencies
-- **User Story 1 (P1)**: Can start after Foundational β No dependencies on other stories
-- **User Story 2 (P2)**: [May depend on US1 scaffold / Can start after Foundational]
-- **User Story 3 (P3)**: [May depend on US1/US2 / Independent after Foundational]
+- **User Story 1 (P1)**: Can start after Foundational (Phase 2) - No dependencies on other stories
+- **User Story 2 (P2)**: Can start after Foundational (Phase 2) - May integrate with US1 but should be independently testable
+- **User Story 3 (P3)**: Can start after Foundational (Phase 2) - May integrate with US1/US2 but should be independently testable
-### Critical Path
+### Within Each User Story
-```
-Phase 1 β Phase 2 β Phase 3 (US1) β ... β Phase N (Polish)
-```
+- Tests (if included) MUST be written and FAIL before implementation
+- Models before services
+- Services before endpoints
+- Core implementation before integration
+- Story complete before moving to next priority
### Parallel Opportunities
-```
-[List tasks that can run in parallel β different files, no dependencies]
+- All Setup tasks marked [P] can run in parallel
+- All Foundational tasks marked [P] can run in parallel (within Phase 2)
+- Once Foundational phase completes, all user stories can start in parallel (if team capacity allows)
+- All tests for a user story marked [P] can run in parallel
+- Models within a story marked [P] can run in parallel
+- Different user stories can be worked on in parallel by different team members
+
+---
+
+## Parallel Example: User Story 1
+
+```bash
+# Launch all tests for User Story 1 together (if tests requested):
+Task: "Contract test for [endpoint] in tests/contract/test_[name].py"
+Task: "Integration test for [user journey] in tests/integration/test_[name].py"
+
+# Launch all models for User Story 1 together:
+Task: "Create [Entity1] model in src/models/[entity1].py"
+Task: "Create [Entity2] model in src/models/[entity2].py"
```
---
@@ -156,25 +220,39 @@ Phase 1 β Phase 2 β Phase 3 (US1) β ... β Phase N (Polish)
### MVP First (User Story 1 Only)
-1. Complete Phase 1: Setup (design gate + infrastructure)
-2. Complete Phase 2: Foundational (ViewModel + accessibility)
+1. Complete Phase 1: Setup
+2. Complete Phase 2: Foundational (CRITICAL - blocks all stories)
3. Complete Phase 3: User Story 1
-4. **STOP and VALIDATE**: Test end-to-end, verify persistence, check TalkBack
-5. Ship as MVP
+4. **STOP and VALIDATE**: Test User Story 1 independently
+5. Deploy/demo if ready
### Incremental Delivery
-1. Phase 1 + Phase 2 β Foundation ready
-2. Phase 3: US1 β **MVP shippable**
-3. Phase 4: US2 β Enhanced experience
-4. Phase N: Polish β Tests + verification β Merge-ready
+1. Complete Setup + Foundational β Foundation ready
+2. Add User Story 1 β Test independently β Deploy/Demo (MVP!)
+3. Add User Story 2 β Test independently β Deploy/Demo
+4. Add User Story 3 β Test independently β Deploy/Demo
+5. Each story adds value without breaking previous stories
### Parallel Team Strategy
With multiple developers:
-1. All complete Phase 1 + Phase 2 together
+1. Team completes Setup + Foundational together
2. Once Foundational is done:
- - Developer A: US1 β US2 β ... *(critical path)*
- - Developer B: Independent stories *(parallel)*
-3. Converge at Phase N (Polish)
+ - Developer A: User Story 1
+ - Developer B: User Story 2
+ - Developer C: User Story 3
+3. Stories complete and integrate independently
+
+---
+
+## Notes
+
+- [P] tasks = different files, no dependencies
+- [Story] label maps task to specific user story for traceability
+- Each user story should be independently completable and testable
+- Verify tests fail before implementing
+- Commit after each task or logical group
+- Stop at any checkpoint to validate story independently
+- Avoid: vague tasks, same file conflicts, cross-story dependencies that break independence
diff --git a/AGENTS.md b/AGENTS.md
index 234f35e7ce..fee116de54 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -49,5 +49,5 @@ You are an expert Android/KMP engineer. Maintain architectural boundaries, use M
<!-- SPECKIT START -->
For additional context about technologies to be used, project structure,
shell commands, and other important information, read the current plan
-at `specs/002-node-list-layout/plan.md`
+at `specs/018-compose-screenshot-testing/plan.md`
<!-- SPECKIT END -->
diff --git a/core/proto/src/main/proto b/core/proto/src/main/proto
index 1c6254062b..b302d92332 160000
--- a/core/proto/src/main/proto
+++ b/core/proto/src/main/proto
@@ -1 +1 @@
-Subproject commit 1c6254062b3f726893b79350aaf8d506eb28503a
+Subproject commit b302d923327402fbe49efcf15ff1b6ef2361b22b
diff --git a/core/ui/detekt-baseline.xml b/core/ui/detekt-baseline.xml
index 77a9a0169e..ab18a483ff 100644
--- a/core/ui/detekt-baseline.xml
+++ b/core/ui/detekt-baseline.xml
@@ -16,7 +16,6 @@
<ID>ComposableParamOrder:SatelliteCountInfo.kt:@Composable fun SatelliteCountInfo</ID>
<ID>ComposableParamOrder:SignalInfo.kt:@Composable fun SignalInfo</ID>
<ID>ComposableParamOrder:SwitchPreference.kt:@Composable fun SwitchPreference</ID>
- <ID>CompositionLocalAllowlist:ContrastLevel.kt:/** * Composition local providing the current [ContrastLevel]. * * Read by components that need to adapt their rendering for accessibility (e.g. message bubbles, signal indicators). */ val LocalContrastLevel = staticCompositionLocalOf { ContrastLevel.STANDARD }</ID>
<ID>CompositionLocalAllowlist:LocalAnalyticsIntroProvider.kt:val LocalAnalyticsIntroProvider = compositionLocalOf<@Composable () -> Unit> { {} }</ID>
<ID>CompositionLocalAllowlist:LocalBarcodeScannerProvider.kt:val LocalBarcodeScannerProvider = compositionLocalOf<@Composable (onResult: (String?) -> Unit) -> BarcodeScanner> { { object : BarcodeScanner { override fun startScan() { // Default NO-OP } } } }</ID>
<ID>CompositionLocalAllowlist:LocalBarcodeScannerProvider.kt:val LocalBarcodeScannerSupported = compositionLocalOf { false }</ID>
@@ -32,7 +31,6 @@
<ID>CompositionLocalAllowlist:MapViewProvider.kt:val LocalMapViewProvider = compositionLocalOf<MapViewProvider?> { null }</ID>
<ID>ContentSlotReused:AdaptiveTwoPane.kt:second: @Composable ColumnScope.() -> Unit</ID>
<ID>FunctionTypeModifierSpacing:Theme.kt:@Composable()</ID>
- <ID>LambdaParameterEventTrailing:MainAppBar.kt:onClickChip: (Node) -> Unit</ID>
<ID>LambdaParameterInRestartableEffect:EmojiPickerDialog.kt:onCategoryChanged: (Int) -> Unit</ID>
<ID>LambdaParameterInRestartableEffect:PlatformUtils.kt:check: () -> Boolean</ID>
<ID>LambdaParameterInRestartableEffect:TracerouteAlertHandler.kt:onNavigateToMap: (destinationNodeNum: Int, requestId: Int, logUuid: String?) -> Unit</ID>
@@ -95,11 +93,40 @@
<ID>PreviewPublic:AlertPreviews.kt:@Preview(showBackground = true, name = "Icon and Text Alert") @Composable fun PreviewIconAlert</ID>
<ID>PreviewPublic:AlertPreviews.kt:@Preview(showBackground = true, name = "Multiple Choice Alert") @Composable fun PreviewMultipleChoiceAlert</ID>
<ID>PreviewPublic:AlertPreviews.kt:@Preview(showBackground = true, name = "Simple Text Alert") @Composable fun PreviewTextAlert</ID>
+ <ID>PreviewPublic:BitwisePreference.kt:@Preview(showBackground = true) @Composable fun BitwisePreferencePreview</ID>
+ <ID>PreviewPublic:ChannelInfo.kt:@PreviewLightDark @Composable fun ChannelInfoPreview</ID>
+ <ID>PreviewPublic:ChannelItem.kt:@Preview @Composable fun ChannelItemPreview</ID>
+ <ID>PreviewPublic:DistanceInfo.kt:@PreviewLightDark @Composable fun DistanceInfoPreview</ID>
+ <ID>PreviewPublic:DropDownPreference.kt:@Preview(showBackground = true) @Composable fun DropDownPreferencePreview</ID>
+ <ID>PreviewPublic:EditIPv4Preference.kt:@Preview(showBackground = true) @Composable fun EditIPv4PreferencePreview</ID>
+ <ID>PreviewPublic:EditListPreference.kt:@Preview(showBackground = true) @Composable fun EditListPreferencePreview</ID>
+ <ID>PreviewPublic:EditPasswordPreference.kt:@Preview(showBackground = true) @Composable fun EditPasswordPreferencePreview</ID>
+ <ID>PreviewPublic:EditTextPreference.kt:@Preview(showBackground = true) @Composable fun EditTextPreferencePreview</ID>
+ <ID>PreviewPublic:ElevationInfo.kt:@Composable @Preview fun ElevationInfoPreview</ID>
+ <ID>PreviewPublic:HopsInfo.kt:@PreviewLightDark @Composable fun HopsInfoPreview</ID>
+ <ID>PreviewPublic:IconInfo.kt:@Composable @Preview fun IconInfoPreview</ID>
+ <ID>PreviewPublic:ImportFab.kt:@Preview(showBackground = true, name = "Channel Context with Sharing") @Composable fun PreviewImportFABChannel</ID>
+ <ID>PreviewPublic:ImportFab.kt:@Preview(showBackground = true, name = "Contact Context") @Composable fun PreviewImportFABContact</ID>
<ID>PreviewPublic:IndoorAirQuality.kt:@Preview(showBackground = true) @Composable fun IAQScalePreview</ID>
+ <ID>PreviewPublic:LastHeardInfo.kt:@PreviewLightDark @Composable fun LastHeardInfoPreview</ID>
<ID>PreviewPublic:LazyColumnDragAndDropDemo.kt:@Preview @Composable fun LazyColumnDragAndDropDemo</ID>
+ <ID>PreviewPublic:ListItem.kt:@Preview(showBackground = true) @Composable fun ListItemDisabledPreview</ID>
+ <ID>PreviewPublic:ListItem.kt:@Preview(showBackground = true) @Composable fun ListItemPreview</ID>
+ <ID>PreviewPublic:ListItem.kt:@Preview(showBackground = true) @Composable fun SwitchListItemPreview</ID>
<ID>PreviewPublic:MaterialBatteryInfo.kt:@PreviewLightDark @Composable fun MaterialBatteryInfoPreview</ID>
+ <ID>PreviewPublic:NodeChip.kt:@Suppress("MagicNumber") @Preview @Composable fun NodeChipPreview</ID>
+ <ID>PreviewPublic:PositionPrecisionPreference.kt:@Preview(showBackground = true) @Composable fun PositionPrecisionPreferencePreview</ID>
+ <ID>PreviewPublic:PreferenceCategory.kt:@Preview(showBackground = true) @Composable fun PreferenceCategoryPreview</ID>
+ <ID>PreviewPublic:RegularPreference.kt:@Preview(showBackground = true) @Composable fun RegularPreferencePreview</ID>
+ <ID>PreviewPublic:SatelliteCountInfo.kt:@PreviewLightDark @Composable fun SatelliteCountInfoPreview</ID>
+ <ID>PreviewPublic:SecurityIcon.kt:@Preview(name = "All Security Icons with Dialog") @Composable fun PreviewAllSecurityIconsWithDialog</ID>
<ID>PreviewPublic:SignalInfo.kt:@Composable @Preview(showBackground = true) fun SignalInfoSimplePreview</ID>
<ID>PreviewPublic:SignalInfo.kt:@PreviewLightDark @Composable fun SignalInfoPreview</ID>
+ <ID>PreviewPublic:SliderPreference.kt:@Suppress("MagicNumber") @Preview(showBackground = true) @Composable fun SliderPreferenceDisabledPreview</ID>
+ <ID>PreviewPublic:SliderPreference.kt:@Suppress("MagicNumber") @Preview(showBackground = true) @Composable fun SliderPreferencePreview</ID>
+ <ID>PreviewPublic:SwitchPreference.kt:@Preview(showBackground = true) @Composable fun SwitchPreferencePreview</ID>
+ <ID>PreviewPublic:TextDividerPreference.kt:@Preview(showBackground = true) @Composable fun TextDividerPreferencePreview</ID>
+ <ID>PreviewPublic:TitledCard.kt:@PreviewLightDark @Composable fun TitledCardPreview</ID>
<ID>ViewModelForwarding:MeshtasticAppShell.kt:MeshtasticCommonAppSetup( uiViewModel = uiViewModel, onNavigateToTracerouteMap = { destNum, requestId, logUuid -> multiBackstack.handleDeepLink( listOf( NodesRoute.NodesGraph, NodeDetailRoute.TracerouteMap(destNum = destNum, requestId = requestId, logUuid = logUuid), ), ) }, )</ID>
<ID>ViewModelForwarding:MeshtasticCommonAppSetup.kt:FirmwareVersionCheck(viewModel = uiViewModel)</ID>
<ID>ViewModelForwarding:MeshtasticCommonAppSetup.kt:SharedDialogs(uiViewModel = uiViewModel)</ID>
diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/BitwisePreference.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/BitwisePreference.kt
index 00c8a3ddcc..211a38359e 100644
--- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/BitwisePreference.kt
+++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/BitwisePreference.kt
@@ -40,6 +40,7 @@ import androidx.compose.ui.unit.dp
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.clear
import org.meshtastic.core.resources.close
+import org.meshtastic.core.ui.theme.AppTheme
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -102,13 +103,15 @@ fun BitwisePreference(
@Preview(showBackground = true)
@Composable
-private fun BitwisePreferencePreview() {
- BitwisePreference(
- title = "Settings",
- value = 3,
- summary = "This is a summary",
- enabled = true,
- items = listOf(1 to "TEST1", 2 to "TEST2"),
- onItemSelected = {},
- )
+fun BitwisePreferencePreview() {
+ AppTheme {
+ BitwisePreference(
+ title = "Settings",
+ value = 3,
+ summary = "This is a summary",
+ enabled = true,
+ items = listOf(1 to "TEST1", 2 to "TEST2"),
+ onItemSelected = {},
+ )
+ }
}
diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/ChannelInfo.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/ChannelInfo.kt
index 4808829c7d..ea5783cb33 100644
--- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/ChannelInfo.kt
+++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/ChannelInfo.kt
@@ -69,6 +69,6 @@ fun ChannelInfo(
@PreviewLightDark
@Composable
-private fun ChannelInfoPreview() {
+fun ChannelInfoPreview() {
AppTheme { ChannelInfo(channel = 2) }
}
diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/ChannelItem.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/ChannelItem.kt
index d266cdcab1..d037c84a09 100644
--- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/ChannelItem.kt
+++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/ChannelItem.kt
@@ -64,6 +64,6 @@ fun ChannelItem(
@Preview
@Composable
-private fun ChannelItemPreview() {
+fun ChannelItemPreview() {
AppTheme { ChannelItem(index = 0, title = "Medium Fast", enabled = true) {} }
}
diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/DistanceInfo.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/DistanceInfo.kt
index 26d2c5f6d1..1d3f0edcc7 100644
--- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/DistanceInfo.kt
+++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/DistanceInfo.kt
@@ -46,6 +46,6 @@ fun DistanceInfo(
@PreviewLightDark
@Composable
-private fun DistanceInfoPreview() {
+fun DistanceInfoPreview() {
AppTheme { DistanceInfo(distance = "423 mi.") }
}
diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/DropDownPreference.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/DropDownPreference.kt
index 3042fdf844..a4239bbd89 100644
--- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/DropDownPreference.kt
+++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/DropDownPreference.kt
@@ -43,6 +43,7 @@ import androidx.compose.ui.graphics.painter.ColorPainter
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
+import org.meshtastic.core.ui.theme.AppTheme
import kotlin.jvm.JvmName
@Composable
@@ -207,13 +208,15 @@ internal expect fun Enum<*>.isDeprecatedEnumEntry(): Boolean
@Preview(showBackground = true)
@Composable
-private fun DropDownPreferencePreview() {
- DropDownPreference(
- title = "Settings",
- summary = "Lorem ipsum dolor sit amet",
- enabled = true,
- items = listOf(DropDownItem("TEST1", "text1"), DropDownItem("TEST2", "text2")),
- selectedItem = "TEST2",
- onItemSelected = {},
- )
+fun DropDownPreferencePreview() {
+ AppTheme {
+ DropDownPreference(
+ title = "Settings",
+ summary = "Lorem ipsum dolor sit amet",
+ enabled = true,
+ items = listOf(DropDownItem("TEST1", "text1"), DropDownItem("TEST2", "text2")),
+ selectedItem = "TEST2",
+ onItemSelected = {},
+ )
+ }
}
diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/EditBase64Preference.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/EditBase64Preference.kt
index 8eb551ae23..bd7a5648e1 100644
--- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/EditBase64Preference.kt
+++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/EditBase64Preference.kt
@@ -50,6 +50,7 @@ import org.meshtastic.core.resources.reset
import org.meshtastic.core.ui.icon.Close
import org.meshtastic.core.ui.icon.MeshtasticIcons
import org.meshtastic.core.ui.icon.Refresh
+import org.meshtastic.core.ui.theme.AppTheme
@Suppress("LongMethod", "CyclomaticComplexMethod", "MagicNumber")
@Composable
@@ -142,14 +143,16 @@ fun EditBase64Preference(
@Preview(showBackground = true)
@Composable
private fun EditBase64PreferencePreview() {
- EditBase64Preference(
- title = "Title",
- summary = "This is a summary",
- value = Channel.getRandomKey(),
- enabled = true,
- keyboardActions = KeyboardActions {},
- onValueChange = { _ -> },
- onGenerateKey = {},
- modifier = Modifier.padding(16.dp),
- )
+ AppTheme {
+ EditBase64Preference(
+ title = "Title",
+ summary = "This is a summary",
+ value = Channel.getRandomKey(),
+ enabled = true,
+ keyboardActions = KeyboardActions {},
+ onValueChange = { _ -> },
+ onGenerateKey = {},
+ modifier = Modifier.padding(16.dp),
+ )
+ }
}
diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/EditIPv4Preference.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/EditIPv4Preference.kt
index 7fa290c703..d3865a475e 100644
--- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/EditIPv4Preference.kt
+++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/EditIPv4Preference.kt
@@ -27,6 +27,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
+import org.meshtastic.core.ui.theme.AppTheme
@Composable
fun EditIPv4Preference(
@@ -68,12 +69,14 @@ fun EditIPv4Preference(
@Preview(showBackground = true)
@Composable
-private fun EditIPv4PreferencePreview() {
- EditIPv4Preference(
- title = "IP Address",
- value = 16820416,
- enabled = true,
- keyboardActions = KeyboardActions {},
- onValueChanged = {},
- )
+fun EditIPv4PreferencePreview() {
+ AppTheme {
+ EditIPv4Preference(
+ title = "IP Address",
+ value = 16820416,
+ enabled = true,
+ keyboardActions = KeyboardActions {},
+ onValueChanged = {},
+ )
+ }
}
diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/EditListPreference.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/EditListPreference.kt
index fd9145cb14..c82b54c924 100644
--- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/EditListPreference.kt
+++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/EditListPreference.kt
@@ -46,6 +46,7 @@ import org.meshtastic.core.resources.name
import org.meshtastic.core.resources.type
import org.meshtastic.core.ui.icon.Close
import org.meshtastic.core.ui.icon.MeshtasticIcons
+import org.meshtastic.core.ui.theme.AppTheme
import org.meshtastic.proto.RemoteHardwarePin
import org.meshtastic.proto.RemoteHardwarePinType
@@ -187,27 +188,29 @@ inline fun <reified T> EditListPreference(
@Preview(showBackground = true)
@Composable
-private fun EditListPreferencePreview() {
- Column {
- EditListPreference(
- title = stringResource(Res.string.ignore_incoming),
- summary = "This is a summary",
- list = listOf(12345, 67890),
- maxCount = 4,
- enabled = true,
- keyboardActions = KeyboardActions {},
- onValuesChanged = {},
- )
- EditListPreference(
- title = "Available pins",
- list =
- listOf(
- RemoteHardwarePin(gpio_pin = 12, name = "Front door", type = RemoteHardwarePinType.DIGITAL_READ),
- ),
- maxCount = 4,
- enabled = true,
- keyboardActions = KeyboardActions {},
- onValuesChanged = {},
- )
+fun EditListPreferencePreview() {
+ AppTheme {
+ Column {
+ EditListPreference(
+ title = stringResource(Res.string.ignore_incoming),
+ summary = "This is a summary",
+ list = listOf(12345, 67890),
+ maxCount = 4,
+ enabled = true,
+ keyboardActions = KeyboardActions {},
+ onValuesChanged = {},
+ )
+ EditListPreference(
+ title = "Available pins",
+ list =
+ listOf(
+ RemoteHardwarePin(gpio_pin = 12, name = "Front door", type = RemoteHardwarePinType.DIGITAL_READ),
+ ),
+ maxCount = 4,
+ enabled = true,
+ keyboardActions = KeyboardActions {},
+ onValuesChanged = {},
+ )
+ }
}
}
diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/EditPasswordPreference.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/EditPasswordPreference.kt
index d99757f176..9eba638d35 100644
--- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/EditPasswordPreference.kt
+++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/EditPasswordPreference.kt
@@ -38,6 +38,7 @@ import org.meshtastic.core.resources.show_password
import org.meshtastic.core.ui.icon.MeshtasticIcons
import org.meshtastic.core.ui.icon.Visibility
import org.meshtastic.core.ui.icon.VisibilityOff
+import org.meshtastic.core.ui.theme.AppTheme
@Composable
fun EditPasswordPreference(
@@ -82,13 +83,15 @@ fun EditPasswordPreference(
@Preview(showBackground = true)
@Composable
-private fun EditPasswordPreferencePreview() {
- EditPasswordPreference(
- title = "Password",
- value = "top secret",
- maxSize = 63,
- enabled = true,
- keyboardActions = KeyboardActions {},
- onValueChanged = {},
- )
+fun EditPasswordPreferencePreview() {
+ AppTheme {
+ EditPasswordPreference(
+ title = "Password",
+ value = "top secret",
+ maxSize = 63,
+ enabled = true,
+ keyboardActions = KeyboardActions {},
+ onValueChanged = {},
+ )
+ }
}
diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/EditTextPreference.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/EditTextPreference.kt
index bd377653a1..d27369c40c 100644
--- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/EditTextPreference.kt
+++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/EditTextPreference.kt
@@ -45,6 +45,7 @@ import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.error
import org.meshtastic.core.ui.icon.Info
import org.meshtastic.core.ui.icon.MeshtasticIcons
+import org.meshtastic.core.ui.theme.AppTheme
@Composable
fun SignedIntegerEditTextPreference(
@@ -269,25 +270,27 @@ fun EditTextPreference(
@Preview(showBackground = true)
@Composable
-private fun EditTextPreferencePreview() {
- Column {
- EditTextPreference(
- title = "String",
- value = "Meshtastic",
- summary = "This is a summary",
- maxSize = 39,
- enabled = true,
- isError = false,
- keyboardOptions = KeyboardOptions.Default,
- keyboardActions = KeyboardActions {},
- onValueChanged = {},
- )
- EditTextPreference(
- title = "Advanced Settings",
- value = UInt.MAX_VALUE.toInt(),
- enabled = true,
- keyboardActions = KeyboardActions {},
- onValueChanged = {},
- )
+fun EditTextPreferencePreview() {
+ AppTheme {
+ Column {
+ EditTextPreference(
+ title = "String",
+ value = "Meshtastic",
+ summary = "This is a summary",
+ maxSize = 39,
+ enabled = true,
+ isError = false,
+ keyboardOptions = KeyboardOptions.Default,
+ keyboardActions = KeyboardActions {},
+ onValueChanged = {},
+ )
+ EditTextPreference(
+ title = "Advanced Settings",
+ value = UInt.MAX_VALUE.toInt(),
+ enabled = true,
+ keyboardActions = KeyboardActions {},
+ onValueChanged = {},
+ )
+ }
}
}
diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/ElevationInfo.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/ElevationInfo.kt
index f1ed821ba1..fb4386b318 100644
--- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/ElevationInfo.kt
+++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/ElevationInfo.kt
@@ -29,6 +29,7 @@ import org.meshtastic.core.resources.altitude
import org.meshtastic.core.resources.elevation_suffix
import org.meshtastic.core.ui.icon.Elevation
import org.meshtastic.core.ui.icon.MeshtasticIcons
+import org.meshtastic.core.ui.theme.AppTheme
import org.meshtastic.proto.Config.DisplayConfig.DisplayUnits
@Composable
@@ -51,6 +52,6 @@ fun ElevationInfo(
@Composable
@Preview
-private fun ElevationInfoPreview() {
- MaterialTheme { ElevationInfo(altitude = 100, system = DisplayUnits.METRIC, suffix = "ASL") }
+fun ElevationInfoPreview() {
+ AppTheme { ElevationInfo(altitude = 100, system = DisplayUnits.METRIC, suffix = "ASL") }
}
diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/HopsInfo.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/HopsInfo.kt
index e7741b4a2a..29dcdbe537 100644
--- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/HopsInfo.kt
+++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/HopsInfo.kt
@@ -42,6 +42,6 @@ fun HopsInfo(hops: Int, modifier: Modifier = Modifier, contentColor: Color = Mat
@PreviewLightDark
@Composable
-private fun HopsInfoPreview() {
+fun HopsInfoPreview() {
AppTheme { HopsInfo(hops = 3) }
}
diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/IconInfo.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/IconInfo.kt
index 570bc52c18..144111cc85 100644
--- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/IconInfo.kt
+++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/IconInfo.kt
@@ -35,6 +35,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import org.meshtastic.core.ui.icon.Elevation
import org.meshtastic.core.ui.icon.MeshtasticIcons
+import org.meshtastic.core.ui.theme.AppTheme
private const val SIZE_ICON = 14
@@ -89,8 +90,8 @@ fun IconInfo(
@Composable
@Preview
-private fun IconInfoPreview() {
- MaterialTheme {
+fun IconInfoPreview() {
+ AppTheme {
IconInfo(icon = MeshtasticIcons.Elevation, contentDescription = "Elevation", label = "Elevation", text = "100m")
}
}
diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/ImportFab.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/ImportFab.kt
index 8e009e150a..c8781e33d5 100644
--- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/ImportFab.kt
+++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/ImportFab.kt
@@ -242,7 +242,7 @@ private fun InputUrlDialog(title: String, onDismiss: () -> Unit, onConfirm: (Str
@Preview(showBackground = true, name = "Contact Context")
@Composable
-private fun PreviewImportFABContact() {
+fun PreviewImportFABContact() {
AppTheme {
Box(modifier = Modifier.fillMaxSize().padding(16.dp)) {
MeshtasticImportFAB(onImport = {}, modifier = Modifier.align(Alignment.BottomEnd), isContactContext = true)
@@ -252,7 +252,7 @@ private fun PreviewImportFABContact() {
@Preview(showBackground = true, name = "Channel Context with Sharing")
@Composable
-private fun PreviewImportFABChannel() {
+fun PreviewImportFABChannel() {
AppTheme {
Box(modifier = Modifier.fillMaxSize().padding(16.dp)) {
MeshtasticImportFAB(
diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/IndoorAirQuality.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/IndoorAirQuality.kt
index 7504c048a0..1150ead035 100644
--- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/IndoorAirQuality.kt
+++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/IndoorAirQuality.kt
@@ -63,6 +63,7 @@ import org.meshtastic.core.resources.show_iaq_legend
import org.meshtastic.core.ui.icon.MeshtasticIcons
import org.meshtastic.core.ui.icon.ThumbUp
import org.meshtastic.core.ui.icon.Warning
+import org.meshtastic.core.ui.theme.AppTheme
import org.meshtastic.core.ui.theme.IAQColors.IAQDangerouslyPolluted
import org.meshtastic.core.ui.theme.IAQColors.IAQExcellent
import org.meshtastic.core.ui.theme.IAQColors.IAQExtremelyPolluted
@@ -254,79 +255,81 @@ fun IAQScale(modifier: Modifier = Modifier) {
@Preview(showBackground = true)
@Composable
fun IAQScalePreview() {
- IAQScale()
+ AppTheme { IAQScale() }
}
@Suppress("LongMethod")
@Preview(showBackground = true)
@Composable
private fun IndoorAirQualityPreview() {
- Column(
- modifier = Modifier.padding(16.dp),
- verticalArrangement = Arrangement.spacedBy(8.dp),
- horizontalAlignment = Alignment.CenterHorizontally,
- ) {
- Text(stringResource(Res.string.preview_pill), style = MaterialTheme.typography.titleLarge)
- Row {
- IndoorAirQuality(iaq = 6)
- IndoorAirQuality(iaq = 51)
- }
- Row {
- IndoorAirQuality(iaq = 101)
- IndoorAirQuality(iaq = 201)
- }
- Row {
- IndoorAirQuality(iaq = 350)
- IndoorAirQuality(iaq = 351)
- }
+ AppTheme {
+ Column(
+ modifier = Modifier.padding(16.dp),
+ verticalArrangement = Arrangement.spacedBy(8.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Text(stringResource(Res.string.preview_pill), style = MaterialTheme.typography.titleLarge)
+ Row {
+ IndoorAirQuality(iaq = 6)
+ IndoorAirQuality(iaq = 51)
+ }
+ Row {
+ IndoorAirQuality(iaq = 101)
+ IndoorAirQuality(iaq = 201)
+ }
+ Row {
+ IndoorAirQuality(iaq = 350)
+ IndoorAirQuality(iaq = 351)
+ }
- Text(stringResource(Res.string.preview_dot), style = MaterialTheme.typography.titleLarge)
- Row {
- IndoorAirQuality(iaq = 6, displayMode = IaqDisplayMode.Dot)
- IndoorAirQuality(iaq = 51, displayMode = IaqDisplayMode.Dot)
- IndoorAirQuality(iaq = 101, displayMode = IaqDisplayMode.Dot)
- IndoorAirQuality(iaq = 201, displayMode = IaqDisplayMode.Dot)
- IndoorAirQuality(iaq = 350, displayMode = IaqDisplayMode.Dot)
- IndoorAirQuality(iaq = 351, displayMode = IaqDisplayMode.Dot)
- }
+ Text(stringResource(Res.string.preview_dot), style = MaterialTheme.typography.titleLarge)
+ Row {
+ IndoorAirQuality(iaq = 6, displayMode = IaqDisplayMode.Dot)
+ IndoorAirQuality(iaq = 51, displayMode = IaqDisplayMode.Dot)
+ IndoorAirQuality(iaq = 101, displayMode = IaqDisplayMode.Dot)
+ IndoorAirQuality(iaq = 201, displayMode = IaqDisplayMode.Dot)
+ IndoorAirQuality(iaq = 350, displayMode = IaqDisplayMode.Dot)
+ IndoorAirQuality(iaq = 351, displayMode = IaqDisplayMode.Dot)
+ }
- Text(stringResource(Res.string.preview_text), style = MaterialTheme.typography.titleLarge)
- Row {
- IndoorAirQuality(iaq = 6, displayMode = IaqDisplayMode.Text)
- IndoorAirQuality(iaq = 51, displayMode = IaqDisplayMode.Text)
- IndoorAirQuality(iaq = 101, displayMode = IaqDisplayMode.Text)
- }
- Row {
- IndoorAirQuality(iaq = 201, displayMode = IaqDisplayMode.Text)
- IndoorAirQuality(iaq = 350, displayMode = IaqDisplayMode.Text)
- IndoorAirQuality(iaq = 500, displayMode = IaqDisplayMode.Text)
- }
+ Text(stringResource(Res.string.preview_text), style = MaterialTheme.typography.titleLarge)
+ Row {
+ IndoorAirQuality(iaq = 6, displayMode = IaqDisplayMode.Text)
+ IndoorAirQuality(iaq = 51, displayMode = IaqDisplayMode.Text)
+ IndoorAirQuality(iaq = 101, displayMode = IaqDisplayMode.Text)
+ }
+ Row {
+ IndoorAirQuality(iaq = 201, displayMode = IaqDisplayMode.Text)
+ IndoorAirQuality(iaq = 350, displayMode = IaqDisplayMode.Text)
+ IndoorAirQuality(iaq = 500, displayMode = IaqDisplayMode.Text)
+ }
- Text(stringResource(Res.string.preview_gauge), style = MaterialTheme.typography.titleLarge)
- Row {
- IndoorAirQuality(iaq = 6, displayMode = IaqDisplayMode.Gauge)
- IndoorAirQuality(iaq = 51, displayMode = IaqDisplayMode.Gauge)
- IndoorAirQuality(iaq = 101, displayMode = IaqDisplayMode.Gauge)
- IndoorAirQuality(iaq = 151, displayMode = IaqDisplayMode.Gauge)
- }
- Row {
- IndoorAirQuality(iaq = 201, displayMode = IaqDisplayMode.Gauge)
- IndoorAirQuality(iaq = 251, displayMode = IaqDisplayMode.Gauge)
- IndoorAirQuality(iaq = 301, displayMode = IaqDisplayMode.Gauge)
- IndoorAirQuality(iaq = 351, displayMode = IaqDisplayMode.Gauge)
- }
- Row {
- IndoorAirQuality(iaq = 401, displayMode = IaqDisplayMode.Gauge)
- IndoorAirQuality(iaq = 500, displayMode = IaqDisplayMode.Gauge)
- }
+ Text(stringResource(Res.string.preview_gauge), style = MaterialTheme.typography.titleLarge)
+ Row {
+ IndoorAirQuality(iaq = 6, displayMode = IaqDisplayMode.Gauge)
+ IndoorAirQuality(iaq = 51, displayMode = IaqDisplayMode.Gauge)
+ IndoorAirQuality(iaq = 101, displayMode = IaqDisplayMode.Gauge)
+ IndoorAirQuality(iaq = 151, displayMode = IaqDisplayMode.Gauge)
+ }
+ Row {
+ IndoorAirQuality(iaq = 201, displayMode = IaqDisplayMode.Gauge)
+ IndoorAirQuality(iaq = 251, displayMode = IaqDisplayMode.Gauge)
+ IndoorAirQuality(iaq = 301, displayMode = IaqDisplayMode.Gauge)
+ IndoorAirQuality(iaq = 351, displayMode = IaqDisplayMode.Gauge)
+ }
+ Row {
+ IndoorAirQuality(iaq = 401, displayMode = IaqDisplayMode.Gauge)
+ IndoorAirQuality(iaq = 500, displayMode = IaqDisplayMode.Gauge)
+ }
- Text(stringResource(Res.string.preview_gradient), style = MaterialTheme.typography.titleLarge)
- IndoorAirQuality(iaq = 6, displayMode = IaqDisplayMode.Gradient)
- IndoorAirQuality(iaq = 51, displayMode = IaqDisplayMode.Gradient)
- IndoorAirQuality(iaq = 101, displayMode = IaqDisplayMode.Gradient)
- IndoorAirQuality(iaq = 201, displayMode = IaqDisplayMode.Gradient)
- IndoorAirQuality(iaq = 351, displayMode = IaqDisplayMode.Gradient)
- IndoorAirQuality(iaq = 401, displayMode = IaqDisplayMode.Gradient)
- IndoorAirQuality(iaq = 500, displayMode = IaqDisplayMode.Gradient)
+ Text(stringResource(Res.string.preview_gradient), style = MaterialTheme.typography.titleLarge)
+ IndoorAirQuality(iaq = 6, displayMode = IaqDisplayMode.Gradient)
+ IndoorAirQuality(iaq = 51, displayMode = IaqDisplayMode.Gradient)
+ IndoorAirQuality(iaq = 101, displayMode = IaqDisplayMode.Gradient)
+ IndoorAirQuality(iaq = 201, displayMode = IaqDisplayMode.Gradient)
+ IndoorAirQuality(iaq = 351, displayMode = IaqDisplayMode.Gradient)
+ IndoorAirQuality(iaq = 401, displayMode = IaqDisplayMode.Gradient)
+ IndoorAirQuality(iaq = 500, displayMode = IaqDisplayMode.Gradient)
+ }
}
}
diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/LastHeardInfo.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/LastHeardInfo.kt
index d59030398c..a8cabaa49f 100644
--- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/LastHeardInfo.kt
+++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/LastHeardInfo.kt
@@ -49,6 +49,6 @@ fun LastHeardInfo(
@PreviewLightDark
@Composable
-private fun LastHeardInfoPreview() {
+fun LastHeardInfoPreview() {
AppTheme { LastHeardInfo(lastHeard = nowSeconds.toInt() - 8600) }
}
diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/LazyColumnDragAndDropDemo.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/LazyColumnDragAndDropDemo.kt
index 2632ab17a2..1a961ccb91 100644
--- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/LazyColumnDragAndDropDemo.kt
+++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/LazyColumnDragAndDropDemo.kt
@@ -63,37 +63,40 @@ import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.preview_footer
import org.meshtastic.core.resources.preview_header
import org.meshtastic.core.resources.preview_item
+import org.meshtastic.core.ui.theme.AppTheme
// Derived in part from:
// https://github.com/androidx/androidx/blob/c92ad2941368202b2d78b8d14c71bf81e9525944/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/LazyColumnDragAndDropDemo.kt
@Preview
@Composable
fun LazyColumnDragAndDropDemo() {
- var list by remember { mutableStateOf(List(50) { it }) }
+ AppTheme {
+ var list by remember { mutableStateOf(List(50) { it }) }
- val listState = rememberLazyListState()
- val dragDropState =
- rememberDragDropState(listState, headerCount = 1) { fromIndex, toIndex ->
- if (fromIndex in list.indices && toIndex in list.indices) {
- list = list.toMutableList().apply { add(toIndex, removeAt(fromIndex)) }
+ val listState = rememberLazyListState()
+ val dragDropState =
+ rememberDragDropState(listState, headerCount = 1) { fromIndex, toIndex ->
+ if (fromIndex in list.indices && toIndex in list.indices) {
+ list = list.toMutableList().apply { add(toIndex, removeAt(fromIndex)) }
+ }
}
- }
- LazyColumn(
- modifier = Modifier.dragContainer(dragDropState = dragDropState, haptics = LocalHapticFeedback.current),
- state = listState,
- contentPadding = PaddingValues(16.dp),
- verticalArrangement = Arrangement.spacedBy(16.dp),
- ) {
- item { Text(stringResource(Res.string.preview_header), Modifier.fillMaxWidth().padding(20.dp)) }
+ LazyColumn(
+ modifier = Modifier.dragContainer(dragDropState = dragDropState, haptics = LocalHapticFeedback.current),
+ state = listState,
+ contentPadding = PaddingValues(16.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp),
+ ) {
+ item { Text(stringResource(Res.string.preview_header), Modifier.fillMaxWidth().padding(20.dp)) }
- itemsIndexed(list, key = { _, item -> item }) { index, item ->
- DraggableItem(dragDropState, index + 1) {
- Card { Text(stringResource(Res.string.preview_item, item), Modifier.fillMaxWidth().padding(20.dp)) }
+ itemsIndexed(list, key = { _, item -> item }) { index, item ->
+ DraggableItem(dragDropState, index + 1) {
+ Card { Text(stringResource(Res.string.preview_item, item), Modifier.fillMaxWidth().padding(20.dp)) }
+ }
}
- }
- item { Text(stringResource(Res.string.preview_footer), Modifier.fillMaxWidth().padding(20.dp)) }
+ item { Text(stringResource(Res.string.preview_footer), Modifier.fillMaxWidth().padding(20.dp)) }
+ }
}
}
diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/ListItem.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/ListItem.kt
index f10e7e5b4a..e34199ac94 100644
--- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/ListItem.kt
+++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/ListItem.kt
@@ -152,19 +152,19 @@ fun ImageVector?.icon(tint: Color = LocalContentColor.current): @Composable (()
@Preview(showBackground = true)
@Composable
-private fun ListItemPreview() {
+fun ListItemPreview() {
AppTheme { ListItem(text = "Text", leadingIcon = MeshtasticIcons.Android, enabled = true) {} }
}
@Preview(showBackground = true)
@Composable
-private fun ListItemDisabledPreview() {
+fun ListItemDisabledPreview() {
AppTheme { ListItem(text = "Text", leadingIcon = MeshtasticIcons.Android, enabled = false) {} }
}
@Preview(showBackground = true)
@Composable
-private fun SwitchListItemPreview() {
+fun SwitchListItemPreview() {
AppTheme { SwitchListItem(text = "Text", leadingIcon = MeshtasticIcons.Android, checked = true, onClick = {}) }
}
diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/NodeChip.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/NodeChip.kt
index 6c096cd25c..a2e6bb2d9f 100644
--- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/NodeChip.kt
+++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/NodeChip.kt
@@ -37,6 +37,7 @@ import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import org.meshtastic.core.model.Node
+import org.meshtastic.core.ui.theme.AppTheme
import org.meshtastic.proto.EnvironmentMetrics
import org.meshtastic.proto.Paxcount
import org.meshtastic.proto.User
@@ -78,8 +79,8 @@ fun NodeChip(modifier: Modifier = Modifier, node: Node, onClick: ((Node) -> Unit
@Suppress("MagicNumber")
@Preview
@Composable
-private fun NodeChipPreview() {
- val user = User(short_name = "\uD83E\uDEE0", long_name = "John Doe")
+fun NodeChipPreview() {
+ val user = User(short_name = "JD", long_name = "John Doe")
val node =
Node(
num = 13444,
@@ -88,5 +89,5 @@ private fun NodeChipPreview() {
paxcounter = Paxcount(ble = 10, wifi = 5),
environmentMetrics = EnvironmentMetrics(temperature = 25f, relative_humidity = 60f),
)
- NodeChip(node = node)
+ AppTheme { NodeChip(node = node) }
}
diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/PositionPrecisionPreference.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/PositionPrecisionPreference.kt
index a828447f74..27a3cf02c6 100644
--- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/PositionPrecisionPreference.kt
+++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/PositionPrecisionPreference.kt
@@ -35,6 +35,7 @@ import org.meshtastic.core.model.util.toDistanceString
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.position_enabled
import org.meshtastic.core.resources.precise_location
+import org.meshtastic.core.ui.theme.AppTheme
import kotlin.math.pow
import kotlin.math.roundToInt
@@ -105,11 +106,13 @@ fun PositionPrecisionPreference(
@Preview(showBackground = true)
@Composable
-private fun PositionPrecisionPreferencePreview() {
- PositionPrecisionPreference(
- value = POSITION_PRECISION_DEFAULT,
- enabled = true,
- onValueChanged = {},
- modifier = Modifier.padding(horizontal = 16.dp),
- )
+fun PositionPrecisionPreferencePreview() {
+ AppTheme {
+ PositionPrecisionPreference(
+ value = POSITION_PRECISION_DEFAULT,
+ enabled = true,
+ onValueChanged = {},
+ modifier = Modifier.padding(horizontal = 16.dp),
+ )
+ }
}
diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/PreferenceCategory.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/PreferenceCategory.kt
index 702bcb4db6..b8fd5b47f2 100644
--- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/PreferenceCategory.kt
+++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/PreferenceCategory.kt
@@ -29,6 +29,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
+import org.meshtastic.core.ui.theme.AppTheme
@Composable
fun PreferenceCategory(
@@ -55,6 +56,6 @@ fun PreferenceCategory(
@Preview(showBackground = true)
@Composable
-private fun PreferenceCategoryPreview() {
- PreferenceCategory(text = "Advanced settings")
+fun PreferenceCategoryPreview() {
+ AppTheme { PreferenceCategory(text = "Advanced settings") }
}
diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/RegularPreference.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/RegularPreference.kt
index b56decacb5..8427a5392e 100644
--- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/RegularPreference.kt
+++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/RegularPreference.kt
@@ -38,6 +38,7 @@ import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
+import org.meshtastic.core.ui.theme.AppTheme
@Composable
fun RegularPreference(
@@ -123,6 +124,6 @@ fun RegularPreference(
@Preview(showBackground = true)
@Composable
-private fun RegularPreferencePreview() {
- RegularPreference(title = "Advanced settings", subtitle = "Text2", onClick = {})
+fun RegularPreferencePreview() {
+ AppTheme { RegularPreference(title = "Advanced settings", subtitle = "Text2", onClick = {}) }
}
diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/SatelliteCountInfo.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/SatelliteCountInfo.kt
index 0cc848502d..ca9f90c4b7 100644
--- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/SatelliteCountInfo.kt
+++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/SatelliteCountInfo.kt
@@ -46,6 +46,6 @@ fun SatelliteCountInfo(
@PreviewLightDark
@Composable
-private fun SatelliteCountInfoPreview() {
+fun SatelliteCountInfoPreview() {
AppTheme { SatelliteCountInfo(satCount = 5) }
}
diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/SecurityIcon.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/SecurityIcon.kt
index e4ba913e40..cd30f750ef 100644
--- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/SecurityIcon.kt
+++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/SecurityIcon.kt
@@ -78,6 +78,7 @@ import org.meshtastic.core.resources.security_icon_insecure_no_precise
import org.meshtastic.core.resources.security_icon_insecure_precise_only
import org.meshtastic.core.resources.security_icon_secure
import org.meshtastic.core.resources.security_icon_warning_precise_mqtt
+import org.meshtastic.core.ui.theme.AppTheme
import org.meshtastic.core.ui.theme.StatusColors.StatusGreen
import org.meshtastic.core.ui.theme.StatusColors.StatusRed
import org.meshtastic.core.ui.theme.StatusColors.StatusYellow
@@ -501,56 +502,61 @@ private fun AllSecurityStates() {
@Preview(name = "Secure Channel Icon")
@Composable
private fun PreviewSecureChannel() {
- SecurityIcon(securityState = SecurityState.SECURE)
+ AppTheme { SecurityIcon(securityState = SecurityState.SECURE) }
}
@Preview(name = "Insecure Precise Icon")
@Composable
private fun PreviewInsecureChannelWithPreciseLocation() {
- SecurityIcon(securityState = SecurityState.INSECURE_PRECISE_ONLY)
+ AppTheme { SecurityIcon(securityState = SecurityState.INSECURE_PRECISE_ONLY) }
}
@Preview(name = "Insecure Channel Icon")
@Composable
private fun PreviewInsecureChannelWithoutPreciseLocation() {
- SecurityIcon(securityState = SecurityState.INSECURE_NO_PRECISE)
+ AppTheme { SecurityIcon(securityState = SecurityState.INSECURE_NO_PRECISE) }
}
@Preview(name = "MQTT Enabled Icon")
@Composable
private fun PreviewMqttEnabled() {
- SecurityIcon(securityState = SecurityState.INSECURE_PRECISE_MQTT_WARNING)
+ AppTheme { SecurityIcon(securityState = SecurityState.INSECURE_PRECISE_MQTT_WARNING) }
}
@Preview(name = "All Security Icons with Dialog")
@Composable
-private fun PreviewAllSecurityIconsWithDialog() {
- var showHelpDialogFor by remember { mutableStateOf<SecurityState?>(null) }
- val stateLabels = remember {
- // Using SecurityState.entries to build the map keys
- mapOf(
- SecurityState.SECURE to "Secure",
- SecurityState.INSECURE_NO_PRECISE to "Insecure (No Precise Location)",
- SecurityState.INSECURE_PRECISE_ONLY to "Insecure (Precise Location Only)",
- SecurityState.INSECURE_PRECISE_MQTT_WARNING to "Insecure (Precise Location + MQTT Warning)",
- )
- }
-
- Column(
- modifier = Modifier.padding(16.dp),
- horizontalAlignment = Alignment.CenterHorizontally,
- verticalArrangement = Arrangement.spacedBy(16.dp),
- ) {
- Text(text = "Security Icons Preview (Click for Help)", style = MaterialTheme.typography.headlineSmall)
+fun PreviewAllSecurityIconsWithDialog() {
+ AppTheme {
+ var showHelpDialogFor by remember { mutableStateOf<SecurityState?>(null) }
+ val stateLabels = remember {
+ // Using SecurityState.entries to build the map keys
+ mapOf(
+ SecurityState.SECURE to "Secure",
+ SecurityState.INSECURE_NO_PRECISE to "Insecure (No Precise Location)",
+ SecurityState.INSECURE_PRECISE_ONLY to "Insecure (Precise Location Only)",
+ SecurityState.INSECURE_PRECISE_MQTT_WARNING to "Insecure (Precise Location + MQTT Warning)",
+ )
+ }
- SecurityState.entries.forEach { state ->
- // Iterate over enum entries
- val label = stateLabels[state] ?: "Unknown State (${state.name})" // Fallback to enum name
- Row(horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically) {
- SecurityIcon(securityState = state, externalOnClick = { showHelpDialogFor = state })
- Text(label)
+ Column(
+ modifier = Modifier.padding(16.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(16.dp),
+ ) {
+ Text(text = "Security Icons Preview (Click for Help)", style = MaterialTheme.typography.headlineSmall)
+
+ SecurityState.entries.forEach { state ->
+ // Iterate over enum entries
+ val label = stateLabels[state] ?: "Unknown State (${state.name})" // Fallback to enum name
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(16.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ SecurityIcon(securityState = state, externalOnClick = { showHelpDialogFor = state })
+ Text(label)
+ }
}
+ showHelpDialogFor?.let { SecurityHelpDialog(securityState = it, onDismiss = { showHelpDialogFor = null }) }
}
- showHelpDialogFor?.let { SecurityHelpDialog(securityState = it, onDismiss = { showHelpDialogFor = null }) }
}
}
diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/SliderPreference.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/SliderPreference.kt
index bfe5352bb5..e47071aeaf 100644
--- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/SliderPreference.kt
+++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/SliderPreference.kt
@@ -79,7 +79,7 @@ fun <T> SliderPreference(
@Suppress("MagicNumber")
@Preview(showBackground = true)
@Composable
-private fun SliderPreferencePreview() {
+fun SliderPreferencePreview() {
val items = listOf(1L to "One", 2L to "Two", 3L to "Three", 4L to "Four", 5L to "Five")
AppTheme {
SliderPreference(
@@ -96,7 +96,7 @@ private fun SliderPreferencePreview() {
@Suppress("MagicNumber")
@Preview(showBackground = true)
@Composable
-private fun SliderPreferenceDisabledPreview() {
+fun SliderPreferenceDisabledPreview() {
val items = listOf(1L to "One", 2L to "Two", 3L to "Three", 4L to "Four", 5L to "Five")
AppTheme {
SliderPreference(
diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/SwitchPreference.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/SwitchPreference.kt
index 4927a33590..fc7764fefa 100644
--- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/SwitchPreference.kt
+++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/SwitchPreference.kt
@@ -31,6 +31,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
+import org.meshtastic.core.ui.theme.AppTheme
@Composable
fun SwitchPreference(
@@ -86,6 +87,6 @@ fun SwitchPreference(
@Preview(showBackground = true)
@Composable
-private fun SwitchPreferencePreview() {
- SwitchPreference(title = "Setting", checked = true, enabled = true, onCheckedChange = {})
+fun SwitchPreferencePreview() {
+ AppTheme { SwitchPreference(title = "Setting", checked = true, enabled = true, onCheckedChange = {}) }
}
diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/TextDividerPreference.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/TextDividerPreference.kt
index e248e1679f..cbce7b67f3 100644
--- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/TextDividerPreference.kt
+++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/TextDividerPreference.kt
@@ -32,6 +32,7 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
+import org.meshtastic.core.ui.theme.AppTheme
@Composable
fun TextDividerPreference(
@@ -76,6 +77,6 @@ fun TextDividerPreference(
@Preview(showBackground = true)
@Composable
-private fun TextDividerPreferencePreview() {
- TextDividerPreference(title = "Advanced settings")
+fun TextDividerPreferencePreview() {
+ AppTheme { TextDividerPreference(title = "Advanced settings") }
}
diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/TitledCard.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/TitledCard.kt
index b76e336169..151da1d9e5 100644
--- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/TitledCard.kt
+++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/TitledCard.kt
@@ -50,6 +50,6 @@ fun TitledCard(title: String?, modifier: Modifier = Modifier, content: @Composab
@PreviewLightDark
@Composable
-private fun TitledCardPreview() {
+fun TitledCardPreview() {
AppTheme { Surface { TitledCard(title = "Title") { Box(modifier = Modifier.fillMaxWidth().height(100.dp)) {} } } }
}
diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/preview/NodePreviewParameterProvider.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/preview/NodePreviewParameterProvider.kt
index 975d51cc16..83735f016a 100644
--- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/preview/NodePreviewParameterProvider.kt
+++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/preview/NodePreviewParameterProvider.kt
@@ -18,7 +18,6 @@ package org.meshtastic.core.ui.component.preview
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import okio.ByteString.Companion.toByteString
-import org.meshtastic.core.model.DeviceMetrics.Companion.currentTime
import org.meshtastic.core.model.Node
import org.meshtastic.proto.Config
import org.meshtastic.proto.DeviceMetrics
@@ -27,7 +26,6 @@ import org.meshtastic.proto.HardwareModel
import org.meshtastic.proto.Paxcount
import org.meshtastic.proto.Position
import org.meshtastic.proto.User
-import kotlin.random.Random
class NodePreviewParameterProvider : PreviewParameterProvider<Node> {
val mickeyMouse =
@@ -42,7 +40,7 @@ class NodePreviewParameterProvider : PreviewParameterProvider<Node> {
role = Config.DeviceConfig.Role.ROUTER,
),
position = Position(latitude_i = 338125110, longitude_i = -1179189760, altitude = 138, sats_in_view = 4),
- lastHeard = currentTime(),
+ lastHeard = 1700000000,
channel = 0,
snr = 12.5F,
rssi = -42,
@@ -60,7 +58,7 @@ class NodePreviewParameterProvider : PreviewParameterProvider<Node> {
val minnieMouse =
mickeyMouse.copy(
- num = Random.nextInt(),
+ num = 1928,
user =
User(
long_name = "Minnie Mouse",
@@ -76,9 +74,9 @@ class NodePreviewParameterProvider : PreviewParameterProvider<Node> {
private val donaldDuck =
Node(
- num = Random.nextInt(),
+ num = 1934,
position = Position(latitude_i = 338052347, longitude_i = -1179208460, altitude = 121, sats_in_view = 66),
- lastHeard = currentTime() - 300,
+ lastHeard = 1699999700,
channel = 0,
snr = 12.5F,
rssi = -42,
@@ -123,7 +121,7 @@ class NodePreviewParameterProvider : PreviewParameterProvider<Node> {
paxcounter = Paxcount(),
)
- private val almostNothing = Node(num = Random.nextInt())
+ private val almostNothing = Node(num = 9999)
override val values: Sequence<Node>
get() =
diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/qr/ScannedQrCodeDialog.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/qr/ScannedQrCodeDialog.kt
index 79671d68e8..a96617957c 100644
--- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/qr/ScannedQrCodeDialog.kt
+++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/qr/ScannedQrCodeDialog.kt
@@ -62,6 +62,7 @@ import org.meshtastic.core.resources.new_channel_rcvd
import org.meshtastic.core.resources.replace
import org.meshtastic.core.resources.replace_channels_and_settings_description
import org.meshtastic.core.ui.component.ChannelSelection
+import org.meshtastic.core.ui.theme.AppTheme
import org.meshtastic.proto.ChannelSet
@Composable
@@ -314,10 +315,14 @@ fun ScannedQrCodeDialog(
@PreviewLightDark
@Composable
private fun ScannedQrCodeDialogPreview() {
- ScannedQrCodeDialog(
- channels = ChannelSet(settings = listOf(Channel.default.settings), lora_config = Channel.default.loraConfig),
- incoming = ChannelSet(settings = listOf(Channel.default.settings), lora_config = Channel.default.loraConfig),
- onDismiss = {},
- onConfirm = {},
- )
+ AppTheme {
+ ScannedQrCodeDialog(
+ channels =
+ ChannelSet(settings = listOf(Channel.default.settings), lora_config = Channel.default.loraConfig),
+ incoming =
+ ChannelSet(settings = listOf(Channel.default.settings), lora_config = Channel.default.loraConfig),
+ onDismiss = {},
+ onConfirm = {},
+ )
+ }
}
diff --git a/docs/BUILD_LOGIC_CONVENTIONS_GUIDE.md b/docs/BUILD_LOGIC_CONVENTIONS_GUIDE.md
deleted file mode 100644
index d3dd5ad933..0000000000
--- a/docs/BUILD_LOGIC_CONVENTIONS_GUIDE.md
+++ /dev/null
@@ -1,282 +0,0 @@
-# Build-Logic Convention Patterns & Guidelines
-
-Quick reference for maintaining and extending the build-logic convention system.
-
-## Core Principles
-
-1. **DRY (Don't Repeat Yourself)**: Extract common configuration into functions
-2. **Clarity Over Cleverness**: Explicit intent in `build.gradle.kts` files matters
-3. **Single Responsibility**: Each convention plugin has one clear purpose
-4. **Test-Driven**: Configuration changes must pass `spotlessCheck`, `detekt`, and tests
-
-## Convention Plugin Architecture
-
-```
-build-logic/
-βββ convention/
-β βββ src/main/kotlin/
-β β βββ KmpFeatureConventionPlugin.kt # KMP feature modules (composes library + compose + koin + common deps)
-β β βββ KmpLibraryConventionPlugin.kt # KMP modules: core libraries
-β β βββ KmpLibraryComposeConventionPlugin.kt # KMP Compose Multiplatform setup
-β β βββ KmpJvmAndroidConventionPlugin.kt # Opt-in jvmAndroidMain hierarchy for Android + desktop JVM
-β β βββ AndroidApplicationConventionPlugin.kt # Main app
-β β βββ AndroidLibraryConventionPlugin.kt # Android-only libraries
-β β βββ AndroidApplicationComposeConventionPlugin.kt
-β β βββ AndroidLibraryComposeConventionPlugin.kt
-β β βββ org/meshtastic/buildlogic/
-β β β βββ KotlinAndroid.kt # Base Kotlin/Android config
-β β β βββ AndroidCompose.kt # Compose setup
-β β β βββ FlavorResolution.kt # Flavor configuration
-β β β βββ MeshtasticFlavor.kt # Flavor definitions
-β β β βββ Detekt.kt # Static analysis
-β β β βββ Spotless.kt # Code formatting
-β β β βββ ... (other config modules)
-```
-
-## How to Add a New Convention
-
-### Example: Adding a new test framework dependency
-
-**Current Pattern (GOOD β
):**
-
-If all KMP modules need a dependency, add it to `KotlinAndroid.kt::configureKmpTestDependencies()`:
-
-```kotlin
-internal fun Project.configureKmpTestDependencies() {
- extensions.configure<KotlinMultiplatformExtension> {
- sourceSets.apply {
- val commonTest = findByName("commonTest") ?: return@apply
- commonTest.dependencies {
- implementation(kotlin("test"))
- // NEW: Add here once, applies to all ~15 KMP modules
- implementation(libs.library("new-test-framework"))
- }
- // ... androidHostTest setup
- }
- }
-}
-```
-
-**Result:** All 15 feature and core modules automatically get the dependency β
-
-### Example: Adding shared `jvmAndroidMain` code to a KMP module
-
-**Current Pattern (GOOD β
):**
-
-If a KMP module needs Java/JVM APIs shared between Android and desktop JVM, apply the opt-in convention plugin instead of manually creating source sets and `dependsOn(...)` edges:
-
-```kotlin
-plugins {
- alias(libs.plugins.meshtastic.kmp.library)
- id("meshtastic.kmp.jvm.android")
-}
-
-kotlin {
- jvm()
- android { /* ... */ }
-
- sourceSets {
- commonMain.dependencies { /* ... */ }
- jvmMain.dependencies { /* jvm-only additions */ }
- androidMain.dependencies { /* android-only additions */ }
- }
-}
-```
-
-**Why:** The convention uses Kotlin's hierarchy template API to create `jvmAndroidMain` without the `Default Kotlin Hierarchy Template Not Applied Correctly` warning triggered by hand-written `dependsOn(...)` graphs.
-
-### Example: Creating a new KMP feature module
-
-**Current Pattern (GOOD β
):**
-
-Use `meshtastic.kmp.feature` for any `feature:*` module. It composes `kmp.library` + `kmp.library.compose` + `koin` and provides all the common Compose/Lifecycle/Koin/Android dependencies that every feature needs:
-
-```kotlin
-plugins {
- alias(libs.plugins.meshtastic.kmp.feature)
- // Optional: add only if this feature needs serialization
- alias(libs.plugins.meshtastic.kotlinx.serialization)
-}
-
-kotlin {
- jvm()
- android {
- namespace = "org.meshtastic.feature.yourfeature"
- androidResources.enable = false
- withHostTest { isIncludeAndroidResources = true }
- }
-
- sourceSets {
- commonMain.dependencies {
- // Only module-SPECIFIC deps here
- implementation(projects.core.common)
- implementation(projects.core.model)
- implementation(projects.core.ui)
- }
- androidMain.dependencies {
- // Only Android-specific extras here
- }
- }
-}
-```
-
-**What the plugin provides automatically:**
-- `commonMain`: `compose-multiplatform-material3`, `jetbrains-lifecycle-viewmodel-compose`, `jetbrains-lifecycle-runtime-compose`, `koin-compose-viewmodel`, `kermit`
-- `androidMain`: `androidx-compose-bom` (platform), `accompanist-permissions`, `androidx-activity-compose`, `androidx-compose-material3`, `androidx-compose-ui-text`, `androidx-compose-ui-tooling-preview`
-- `commonTest`: `core:testing`
-
-**Why:** Eliminates ~15 duplicate dependency declarations per feature module (modelled after Now in Android's `AndroidFeatureImplConventionPlugin`).
-
-### Example: Adding Android-specific test config
-
-**Pattern:** Test options (`animationsDisabled`, `testInstrumentationRunner`, `unitTests.isReturnDefaultValues`) are centralized in `configureKotlinAndroid()` via `CommonExtension`, so they apply to both app and library modules automatically. To add new test config, update `KotlinAndroid.kt::configureKotlinAndroid()`:
-
-```kotlin
-internal fun Project.configureKotlinAndroid(
- commonExtension: CommonExtension<*, *, *, *, *, *>,
-) {
- commonExtension.apply {
- testOptions {
- animationsDisabled = true
- unitTests.isReturnDefaultValues = true
- // NEW: Add shared test options here
- }
- }
-}
-```
-
-## Duplication Heuristics
-
-**When to consolidate (DRY):**
-- β
Configuration appears in 3+ convention plugins
-- β
The duplication changes together (same reasons to update)
-- β
Extraction doesn't require complex type gymnastics
-- β
Underlying Gradle extension is the same (`CommonExtension`)
-
-**When to keep separate (Clarity):**
-- β
Different Gradle extension types (`ApplicationExtension` vs `LibraryExtension`)
-- β
Plugin intent is explicit in `build.gradle.kts` usage
-- β
Duplication is small (<50 lines) and stable
-- β
Future divergence between app/library handling is plausible
-
-**Examples in codebase:**
-
-| Duplication | Status | Reasoning |
-|-------------|--------|-----------|
-| `AndroidApplicationComposeConventionPlugin` β `AndroidLibraryComposeConventionPlugin` | **Kept Separate** | Different extension types; small duplication; explicit intent |
-| `AndroidApplicationFlavorsConventionPlugin` β `AndroidLibraryFlavorsConventionPlugin` | **Kept Separate** | Different extension types; small duplication; explicit intent |
-| `configureKmpTestDependencies()` (7 modules) | **Consolidated** | Large duplication; single source of truth; all KMP modules benefit |
-| `jvmAndroidMain` hierarchy setup (4 modules) | **Consolidated** | Shared KMP hierarchy pattern; avoids manual `dependsOn(...)` edges and hierarchy warnings |
-| `PUBLISHED_MODULES` set (4 usages) | **Consolidated** | Was repeated as `listOf(...)` in 4 places; now a single `setOf(...)` constant in `KotlinAndroid.kt` |
-| `SHARED_COMPILER_ARGS` list (2 code paths) | **Consolidated** | Eliminates duplicated `-opt-in` flags between KMP target compilations and `KotlinCompile` task configuration |
-
-## Testing Convention Changes
-
-After modifying a convention plugin, verify:
-
-```bash
-# 1. Code quality
-./gradlew spotlessCheck detekt
-
-# 2. Compilation
-./gradlew assembleDebug assembleRelease
-
-# 3. Tests
-./gradlew test # All unit tests
-./gradlew :feature:messaging:jvmTest # Feature module tests
-./gradlew :feature:node:testAndroidHostTest # Android host tests
-```
-
-## Documentation Requirements
-
-When you add/modify a convention:
-
-1. **Add Kotlin docs** to the function:
- ```kotlin
- /**
- * Configure test dependencies for KMP modules.
- *
- * Automatically applies kotlin("test") to:
- * - commonTest source set (all targets)
- * - androidHostTest source set (Android-only)
- *
- * Usage: Called automatically by KmpLibraryConventionPlugin
- */
- internal fun Project.configureKmpTestDependencies() { ... }
- ```
-
-2. **Update AGENTS.md** if convention affects developers
-3. **Update this guide** if pattern changes
-
-## Performance Tips
-
-- **Configuration-time:** Convention logic runs during Gradle configuration (0.5-2s)
-- **Build-time:** No impact (conventions don't execute tasks)
-- **Optimization focus:** Minimize `extensions.configure()` blocks (lazy evaluation is preferred)
-
-### Good β
-```kotlin
-extensions.configure<KotlinMultiplatformExtension> {
- // Single block for all source set configuration
- sourceSets.apply {
- commonTest.dependencies { /* ... */ }
- androidHostTest?.dependencies { /* ... */ }
- }
-}
-```
-
-### Avoid β
-```kotlin
-// Multiple blocks - slower configuration
-extensions.configure<KotlinMultiplatformExtension> {
- sourceSets.getByName("commonTest").dependencies { /* ... */ }
-}
-extensions.configure<KotlinMultiplatformExtension> {
- sourceSets.getByName("androidHostTest").dependencies { /* ... */ }
-}
-```
-
-## Common Pitfalls
-
-### β **Mistake: Adding dependencies in the wrong place**
-```kotlin
-// WRONG: Adds to ALL modules, not just KMP
-extensions.configure<Project> {
- dependencies { add("implementation", ...) } // Global!
-}
-
-// RIGHT: Scoped to specific source set/module type
-commonTest.dependencies { implementation(...) }
-```
-
-### β **Mistake: Extension type mismatch**
-```kotlin
-// WRONG: LibraryExtension isn't a subtype of ApplicationExtension
-extensions.configure<ApplicationExtension> {
- // Won't apply to library modules
-}
-
-// RIGHT: Use CommonExtension or specific types
-extensions.configure<CommonExtension> {
- // Applies to both
-}
-```
-
-### β **Mistake: Side effects during configuration**
-```kotlin
-// WRONG: Eager task configuration at plugin-apply time
-tasks.withType<Test> {
- // Can realize tasks too early
-}
-
-// RIGHT: Lazy, configuration-cache-friendly wiring
-tasks.withType<Test>().configureEach {
- // Applies to existing and future tasks lazily
-}
-```
-
-## Related Files
-
-- `AGENTS.md` - Development guidelines (Section 3.B testing, Section 4.A build protocol)
-- `build-logic/convention/build.gradle.kts` - Convention plugin build config
-
diff --git a/docs/decisions/ble-strategy.md b/docs/decisions/ble-strategy.md
deleted file mode 100644
index 304150913b..0000000000
--- a/docs/decisions/ble-strategy.md
+++ /dev/null
@@ -1,27 +0,0 @@
-# Decision: BLE KMP Strategy
-
-> Date: 2026-03-16 | Status: **Decided β Fully Migrated to Kable**
-
-## Context
-
-`core:ble` needed to support non-Android targets. Nordic's Kotlin-BLE-Library, while mature on Android and actively tested in the app, was primarily Android/iOS focused and lacked support for Desktop (JVM) targets. Kable natively supports all Kotlin Multiplatform targets (Android, Apple, Desktop/JVM, Web).
-
-Initially, we implemented an **Interface-Driven "Nordic Hybrid" Abstraction** (keeping Nordic on Android behind `commonMain` interfaces) to wait and see if Nordic expanded their KMP support.
-
-However, as Desktop integration advanced, we found the need for a unified BLE transport.
-
-## Decision
-
-**Migrate entirely to Kable:**
-
-- We migrated all BLE transport logic across Android and Desktop to use Kable.
-- The `commonMain` interfaces (`BleConnection`, `BleScanner`, `BleDevice`, `BluetoothRepository`, etc.) remain, but their core implementations (`KableBleConnection`, `KableBleScanner`) are now entirely shared in `commonMain`.
-- The Android-specific Nordic dependencies (`no.nordicsemi.kotlin.ble:*`) and the Nordic DFU library were completely excised from the project.
-- OTA Firmware updates were successfully refactored to use the Kable-based `BleOtaTransport`, shared across Android and Desktop in `commonMain`.
-- Nordic Secure DFU was reimplemented as a pure KMP protocol stack (`SecureDfuTransport`, `SecureDfuProtocol`, `SecureDfuHandler`) using Kable, with no dependency on the Nordic DFU library.
-
-## Consequences
-
-- **Maximal Code Deduplication:** The BLE implementation is completely shared across Android and Desktop in `core:ble/commonMain`.
-- **Future-Proofing:** Adding an `iosMain` target in the future will be trivial, as it can leverage the same shared Kable abstractions.
-- **Lost Nordic Mocks:** Kable lacks the comprehensive mock infrastructure of the Nordic library. Consequently, several complex BLE OTA unit tests had to be deprecated. Re-establishing this test coverage using custom Kable fakes is an ongoing technical debt item.
diff --git a/docs/decisions/koin-migration.md b/docs/decisions/koin-migration.md
deleted file mode 100644
index fcaf8b2dba..0000000000
--- a/docs/decisions/koin-migration.md
+++ /dev/null
@@ -1,34 +0,0 @@
-# Decision: Hilt β Koin Migration
-
-> Date: 2026-02-20 to 2026-03-09 | Status: **Complete**
-
-## Context
-
-Hilt (Dagger) was the strongest remaining barrier to KMP adoption β it requires Android-specific annotation processing and can't run in `commonMain`.
-
-## Decision
-
-Migrated to **Koin 4.2.0-RC1** with the **K2 Compiler Plugin** (`io.insert-koin.compiler.plugin`) and later upgraded to **0.4.1**.
-
-Key choices:
-- `@KoinViewModel` replaces `@HiltViewModel`; `koinViewModel()` replaces `hiltViewModel()`
-- `@Module` + `@ComponentScan` in `commonMain` modules (valid 2026 KMP pattern)
-- `@KoinWorker` replaces `@HiltWorker` for WorkManager
-- `@InjectedParam` replaces `@Assisted` for factory patterns
-- Root graph assembly centralized in `AppKoinModule`; shared modules expose annotated definitions
-- **Koin 0.4.1 A1 Compile Safety Disabled:** Meshtastic heavily utilizes dependency inversion across KMP modules (e.g., interfaces defined in `core:repository` are implemented in `core:data`). Koin 0.4.x's per-module A1 validation strictly enforces that all dependencies must be explicitly provided or included locally, breaking this clean architecture. We have globally disabled A1 `compileSafety` in `KoinConventionPlugin` to properly rely on Koin's A3 full-graph validation at the composition root (`startKoin`).
-
-## Gotchas Discovered
-
-1. **K2 Compiler Plugin signature collision:** Multiple `@Single` providers with identical JVM signatures in the same `@Module` cause `ClassCastException`. Fix: split into separate `@Module` classes.
-2. **Circular dependencies:** `Lazy<T>` injection can still `StackOverflowError` if `Lazy` is accessed too early (e.g., in `init` coroutine). Fix: pass dependencies as function parameters instead.
-3. **Robolectric `KoinApplicationAlreadyStartedException`:** Call `stopKoin()` in `onTerminate`.
-
-## Consequences
-
-- Hilt completely removed
-- All 23 KMP modules can contain Koin-annotated definitions
-- Desktop bootstraps its own `DesktopKoinModule` with stubs + real implementations
-- 11 passthrough Android ViewModel wrappers eliminated
-
-
diff --git a/docs/decisions/testing-consolidation-2026-03.md b/docs/decisions/testing-consolidation-2026-03.md
deleted file mode 100644
index 06612cc4f0..0000000000
--- a/docs/decisions/testing-consolidation-2026-03.md
+++ /dev/null
@@ -1,38 +0,0 @@
-<!--
- - Copyright (c) 2026 Meshtastic LLC
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU General Public License as published by
- - the Free Software Foundation, either version 3 of the License, or
- - (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU General Public License for more details.
- -
- - You should have received a copy of the GNU General Public License
- - along with this program. If not, see <https://www.gnu.org/licenses/>.
- -->
-
-# Decision: Testing Consolidation β `core:testing` Module
-
-**Date:** 2026-03-11
-**Status:** Implemented
-
-## Context
-
-Each KMP module independently declared scattered test dependencies (`junit`, `mockk`, `coroutines-test`, `turbine`), leading to version drift and duplicated test doubles across modules.
-
-## Decision
-
-Created `core:testing` as a lightweight shared module for test doubles, fakes, and utilities. It depends only on `core:model` and `core:repository` (no heavy deps like `core:database`). All modules declare `implementation(projects.core.testing)` in `commonTest` to get a unified test dependency set.
-
-## Consequences
-
-- **Single source** for test fakes (`FakeRadioController`, `FakeNodeRepository`, `TestDataFactory`)
-- **Clean dependency graph** β `core:testing` is lightweight; heavy modules depend on it in test scope, not vice versa
-- **No production leakage** β only declared in `commonTest`, never in release artifacts
-- **Reduced maintenance** β updating test libraries touches one `build.gradle.kts`
-
-See [`core/testing/README.md`](../../core/testing/README.md) for usage guide and API reference.
diff --git a/docs/kmp-status.md b/docs/kmp-status.md
deleted file mode 100644
index 1e65524372..0000000000
--- a/docs/kmp-status.md
+++ /dev/null
@@ -1,178 +0,0 @@
-# KMP Migration Status
-
-> Last updated: 2026-04-15
-
-Single source of truth for Kotlin Multiplatform migration progress. For the forward-looking roadmap, see [`roadmap.md`](./roadmap.md). For completed decision records, see [`decisions/`](./decisions/).
-
-## Summary
-
-Meshtastic-Android has completed its **Android-first structural KMP migration** across core logic and feature modules, with **full JVM cross-compilation validated in CI**. The desktop target has a working Navigation 3 shell, TCP transport with full mesh handshake, and multiple features wired with real screens.
-
-Modules that share JVM-specific code between Android and desktop now standardize on the `meshtastic.kmp.jvm.android` convention plugin, which creates `jvmAndroidMain` via Kotlin's hierarchy template API instead of manual `dependsOn(...)` source-set wiring.
-
-## Module Inventory
-
-### Core Modules (21 total)
-
-| Module | KMP? | JVM target? | Notes |
-|---|:---:|:---:|---|
-| `core:proto` | β
| β
| Protobuf definitions |
-| `core:common` | β
| β
| Utilities, `jvmAndroidMain` source set |
-| `core:model` | β
| β
| Domain models, `jvmAndroidMain` source set |
-| `core:repository` | β
| β
| Domain interfaces |
-| `core:di` | β
| β
| Dispatchers, qualifiers |
-| `core:navigation` | β
| β
| Shared Navigation 3 routes |
-| `core:resources` | β
| β
| Compose Multiplatform resources |
-| `core:datastore` | β
| β
| Multiplatform DataStore |
-| `core:database` | β
| β
| Room KMP |
-| `core:domain` | β
| β
| UseCases |
-| `core:prefs` | β
| β
| Preferences layer |
-| `core:network` | β
| β
| Ktor, `StreamFrameCodec`, `TcpTransport`, `SerialTransport`, `BleRadioTransport` |
-| `core:data` | β
| β
| Data orchestration |
-| `core:ble` | β
| β
| Kable multiplatform BLE abstractions in commonMain |
-| `core:nfc` | β
| β
| NFC contract in commonMain; hardware in androidMain |
-| `core:service` | β
| β
| Service layer; Android bindings in androidMain |
-| `core:ui` | β
| β
| Shared Compose UI, pure KMP QR generator, `jvmAndroidMain` + `jvmMain` actuals |
-| `core:testing` | β
| β
| Shared test doubles, fakes, and utilities for `commonTest` |
-| `core:takserver` | β
| β
| TAK/ATAK integration, Fountain codec |
-| `core:api` | β | β | Android-only (AIDL). Intentional. |
-| `core:barcode` | β | β | Android-only (CameraX). Flavor split minimised to decoder factory only (ML Kit / ZXing). Shared contract in `core:ui`. |
-
-**19/21** core modules are KMP with JVM targets. The 2 Android-only modules are intentionally platform-specific, with shared contracts already abstracted into `core:ui/commonMain`.
-
-### Feature Modules (9 total β 9 KMP with JVM, 1 Android-only widget)
-
-| Module | UI in commonMain? | Desktop wired? |
-|---|:---:|:---:|
-| `feature:settings` | β
| β
~35 real screens; fully shared `settingsGraph` and UI |
-| `feature:node` | β
| β
Adaptive list-detail; fully shared `nodesGraph`, `PositionLogScreen`, and `NodeContextMenu` |
-| `feature:messaging` | β
| β
Adaptive contacts + messages; fully shared `contactsGraph`, `MessageScreen`, `ContactsScreen`, and `MessageListPaged` |
-| `feature:connections` | β
| β
Shared `ConnectionsScreen` with dynamic transport detection |
-| `feature:intro` | β | β | Screens remain in `androidMain`; shared ViewModel only |
-| `feature:map` | β | Placeholder; shared `NodeMapViewModel`, `BaseMapViewModel`. Map rendering decomposed into 3 `CompositionLocal` provider contracts (`MapViewProvider`, `NodeTrackMapProvider`, `TracerouteMapProvider`) with per-flavor implementations in `:app` |
-| `feature:firmware` | β
| β
Fully KMP: Unified OTA, native Secure DFU, USB/UF2, FirmwareRetriever |
-| `feature:wifi-provision` | β
| β
KMP WiFi provisioning via BLE (Nymea protocol); shared UI and ViewModel |
-| `feature:widget` | β | β | Android-only (Glance appwidgets). Intentional. |
-
-### Desktop Module
-
-Working Compose Desktop application with:
-- Navigation 3 shell (`NavigationRail` + `NavDisplay`) using shared routes
-- Full Koin DI graph (stubs + real implementations)
-- TCP, Serial/USB, and BLE transports with auto-reconnect and full `want_config` handshake
-- Adaptive list-detail screens for nodes and contacts
-- **Dynamic Connections screen** with automatic discovery of platform-supported transports (TCP, Serial/USB, BLE)
-- **Desktop language picker** backed by `UiPreferencesDataSource.locale`, with immediate Compose Multiplatform resource updates
-- **Navigation-preserving locale switching** via `Main.kt` `staticCompositionLocalOf` recomposition instead of recreating the Nav3 backstack
-- Node detail metrics screens (Device, Environment, Signal, Power, Pax) wired with shared KMP + Vico charts
-- **Feature-driven Architecture:** Desktop navigation completely relies on feature modules via `commonMain` exported graphs (`settingsGraph`, `nodesGraph`, `contactsGraph`, etc.), reducing the desktop module to a simple host shell.
-- **Native notifications and system tray icon** wired via `DesktopNotificationManager`
-- **Native release pipeline** generating `.dmg` (macOS), `.msi` (Windows), and `.deb` (Linux) installers in CI
-
-## Scorecard
-
-| Area | Score | Notes |
-|---|---|---|
-| Shared business/data logic | **9/10** | All core layers shared; RadioTransport interface unified |
-| Shared feature/UI logic | **9/10** | 9 KMP feature modules; firmware fully migrated; wifi-provision added; `feature:intro` and `feature:map` share ViewModels but UI remains in `androidMain` |
-| Android decoupling | **9/10** | No known `java.*` calls in `commonMain`; app module extraction in progress (navigation, connections, background services, and widgets extracted) |
-| Multi-target readiness | **9/10** | Full JVM; release-ready desktop; iOS simulator builds compiling successfully |
-| CI confidence | **9/10** | 26 modules validated (including feature:wifi-provision); native release installers automated |
-| DI portability | **8/10** | Koin annotations in commonMain; supportedDeviceTypes injected per platform |
-| Test maturity | **9/10** | Mokkery, Turbine, and Kotest integrated; property-based testing established; broad coverage across all 9 features. SfppHasher, AddressUtils, formatString hex, and MetricFormatter edge cases newly covered. Gaps: `core:service`, `core:network` (TcpTransport), `core:ble` state machine, `core:ui` utils |
-
-## Completion Estimates
-
-| Lens | % |
-|---|---:|
-| Android-first structural KMP | ~100% |
-| Shared business logic | ~98% |
-| Shared feature/UI | ~92% |
-| True multi-target readiness | ~85% |
-| "Add iOS without surprises" | ~100% |
-
-## Proposed Next Steps for KMP Migration
-
-Based on the latest codebase investigation, the following steps are proposed to complete the multi-target and iOS-readiness migrations:
-
-1. **Wire Desktop Features:** Complete desktop UI wiring for `feature:intro` and implement a shared fallback for `feature:map` (which is currently a placeholder on desktop).
-2. **Flesh out iOS Actuals:** Complete the actual implementations for iOS UI stubs (e.g., `AboutLibrariesLoader`, `rememberOpenMap`, `SettingsMainScreen`) that were recently added to unblock iOS compilation.
-3. **Boot iOS Target:** Set up an initial skeleton Xcode project to start running the now-compiling `iosSimulatorArm64` / `iosArm64` binaries on a real simulator/device.
-
-## Key Architecture Decisions
-
-| Decision | Status | Details |
-|---|---|---|
-| Navigation 3 parity model (shared `TopLevelDestination` + platform adapters) | β
Done | Both shells use shared `TopLevelDestination` enum and `MeshtasticNavDisplay` from `core:ui/commonMain`; parity tests in `core:navigation/commonTest` |
-| Hilt β Koin | β
Done | See [`decisions/koin-migration.md`](./decisions/koin-migration.md) |
-| BLE abstraction (Kable) | β
Done | See [`decisions/ble-strategy.md`](./decisions/ble-strategy.md) |
-| Firmware KMP migration (pure Secure DFU) | β
Done | Native Nordic Secure DFU protocol reimplemented in pure KMP using Kable; desktop is first-class target |
-| Material 3 Adaptive (JetBrains) | β
Done | Version `1.3.0-alpha06` aligned with CMP `1.11.0-beta02`; supports Large (1200dp) and Extra-large (1600dp) breakpoints |
-| JetBrains lifecycle/nav3 alias alignment | β
Done | All forked deps use `jetbrains-*` prefix in version catalog; `core:data` commonMain uses JetBrains lifecycle runtime |
-| Expect/actual consolidation | β
Done | 10+ pairs eliminated (including `formatString`, `CommonUri`, `SfppHasher`); ~20 genuinely platform-specific retained (Parcelable, DateFormatter, Database, Location, Composable UI primitives) |
-| Transport deduplication | β
Done | `StreamFrameCodec`, `TcpTransport`, and `SerialTransport` shared in `core:network` |
-| **Transport Lifecycle Unification** | β
Done | `SharedRadioInterfaceService` orchestrates auto-reconnect, connection state, and heartbeat uniformly across Android and Desktop. |
-| **Database Parity** | β
Done | `DatabaseManager` is pure KMP, giving iOS and Desktop support for multiple connected nodes with LRU caching. On JVM/Desktop, inactive databases are explicitly closed on switch (Room KMP's `setAutoCloseTimeout` is Android-only), and `desktopDataDir()` in `core:database/jvmMain` is the single source for data directory resolution. |
-| Emoji picker unification | β
Done | Single commonMain implementation replacing 3 platform variants |
-| Cross-platform deduplication pass | β
Done | Extracted shared `AlertHost`, `SharedDialogs`, `PlaceholderScreen`, `ThemePickerDialog`, `MeshtasticNavDisplay`, `formatLogsTo()`, `handleNodeAction()`, `findNodeByNameSuffix()`, `MeshtasticAppShell`, `BleRadioTransport`, and `BaseRadioTransportFactory` to `commonMain`; eliminated ~1,200 lines of duplicated Compose UI code across Android/desktop |
-| URI unification | β
Done | `CommonUri` is a `typealias` to `com.eygraber.uri.Uri` (uri-kmp); `MeshtasticUri` wrapper deleted; bridge with `toAndroidUri()`/`toKmpUri()` |
-| Utility commonization | β
Done | `formatString` β pure Kotlin parser in `commonMain`; `SfppHasher` and `CryptoCodec` β `Okio ByteString.sha256()`; `MetricFormatter` centralizes display strings (temperature, voltage, current, %, humidity, pressure, SNR, RSSI) |
-
-## Navigation Parity Note
-
-- Desktop and Android both use the shared `TopLevelDestination` enum from `core:navigation/commonMain` β no separate `DesktopDestination` remains.
-- Both shells utilize the **Navigation 3 Scene-based architecture**, allowing for multi-pane layouts (e.g., three-pane on Large/XL displays) using shared routes.
-- Both shells iterate `TopLevelDestination.entries` with shared icon mapping from `core:ui` (`TopLevelDestinationExt.icon`).
-- Desktop locale changes now trigger a full subtree recomposition from `Main.kt` without resetting the shared Navigation 3 backstack, so translated labels update in place.
-- Firmware remains available as an in-flow route instead of a top-level destination, matching Android information architecture.
-- Android navigation graphs are decoupled and extracted into their respective feature modules, aligning with the Desktop architecture.
-- Parity tests exist in `core:navigation/commonTest` (`NavigationParityTest`) and `desktop/test` (`DesktopTopLevelDestinationParityTest`).
-- Remaining parity work: serializer registration validation and platform exception tracking.
-
-## App Module Thinning Status
-
-All major ViewModels have now been extracted to `commonMain` and no longer rely on Android-specific subclasses. Platform-specific dependencies (like `android.net.Uri` or Location permissions) have been successfully isolated behind injected `core:repository` interfaces (e.g., `FileService`, `LocationService`).
-
-**The extraction of all feature-specific navigation graphs, background services, and widgets out of `:app` is complete.** The `:app` module now only serves as the root DI assembler and NavHost container.
-
-Extracted to shared `commonMain` (no longer app-only):
-- `SettingsViewModel` β `feature:settings/commonMain`
-- `RadioConfigViewModel` β `feature:settings/commonMain`
-- `DebugViewModel` β `feature:settings/commonMain`
-- `MetricsViewModel` β `feature:node/commonMain`
-- `UIViewModel` β `core:ui/commonMain`
-- `ChannelViewModel` β `feature:settings/commonMain`
-- `NodeMapViewModel` β `feature:map/commonMain` (Shared logic for node-specific maps)
-- `BaseMapViewModel` β `feature:map/commonMain` (Core contract for all maps)
-- `TracerouteOverlay` β `core:model/commonMain` (Pure data class for traceroute route segments; extracted from `feature:map` for cross-module reuse)
-- `GeoConstants` β `core:model/commonMain` (Centralized `DEG_D`, `HEADING_DEG`, `EARTH_RADIUS_METERS` constants; eliminates 7 duplicate private constants)
-
-Extracted to core KMP modules:
-- Android Services, WorkManager Workers, and BroadcastReceivers β `core:service/androidMain`
-- USB/Serial radio connections β `core:network/androidMain`
-- TCP radio connections, BLE radio connections (`BleRadioTransport`), and mDNS/NSD Service Discovery β `core:network/commonMain` (with Android `NsdManager` and Desktop `JmDNS` implementations)
-
-Remaining to be extracted from `:app` or unified in `commonMain`:
-- `MapViewModel` (Unify Google/F-Droid flavors into a single `commonMain` class consuming a `MapConfigProvider` interface. `MapViewProvider` interface simplified β track rendering and traceroute rendering extracted to dedicated provider contracts)
-
-## Prerelease Dependencies
-
-| Dependency | Version | Why |
-|---|---|---|
-| Compose Multiplatform | `1.11.0-beta02` | Required for JetBrains Adaptive `1.3.0-alpha06` and Material 3 `1.11.0-alpha06` |
-| Compose Multiplatform Material 3 | `1.11.0-alpha06` | Material 3 components including `NavigationSuiteScaffold` |
-| Koin | `4.2.1` | Nav3 + K2 compiler plugin support |
-| JetBrains Lifecycle | `2.11.0-alpha03` | Multiplatform ViewModel/lifecycle; includes `lifecycle-viewmodel-navigation3` for entry-scoped ViewModels |
-| JetBrains Navigation 3 | `1.1.0-rc01` | Multiplatform navigation with Scene architecture, `NavEntry.metadata`, transition specs |
-| JetBrains Navigation Event | `1.1.0-alpha01` | KMP `NavigationBackHandler` for predictive back |
-| JetBrains Material 3 Adaptive | `1.3.0-alpha06` | `ListDetailPaneScaffold`, `ThreePaneScaffold`, Large/XL breakpoints |
-| Kable BLE | `0.42.0` | Provides fully multiplatform BLE support |
-
-**Policy:** Stable by default. RC when it unlocks KMP functionality. Alpha only behind hard abstraction seams. Do not downgrade CMP or Koin β they enable critical KMP features.
-
-## References
-
-- Roadmap: [`docs/roadmap.md`](./roadmap.md)
-- Agent guide: [`AGENTS.md`](../AGENTS.md)
-- Agent skills: [`.skills/`](../.skills/)
-- Decision records: [`docs/decisions/`](./decisions/)
diff --git a/docs/roadmap.md b/docs/roadmap.md
deleted file mode 100644
index 02e7809e30..0000000000
--- a/docs/roadmap.md
+++ /dev/null
@@ -1,116 +0,0 @@
-# Roadmap
-
-> Last updated: 2026-04-15
-
-Forward-looking priorities for the Meshtastic KMP multi-target effort. For current state, see [`kmp-status.md`](./kmp-status.md).
-
-## Architecture Health (Immediate)
-
-These items address structural gaps identified in the March 2026 architecture review. They are prerequisites for safe multi-target expansion.
-
-| Item | Impact | Effort | Status |
-|---|---|---|---|
-| Purge `java.util.Locale` from `commonMain` (3 files) | High | Low | β
|
-| Replace `ConcurrentHashMap` in `commonMain` (3 files) | High | Low | β
|
-| Create `core:testing` shared test fixtures | Medium | Low | β
|
-| Add feature module `commonTest` (settings, node, messaging) | Medium | Medium | β
|
-| Desktop Koin `checkModules()` integration test | Medium | Low | β
|
-| Auto-wire Desktop ViewModels via K2 Compiler (eliminate manual wiring) | Medium | Low | β
|
-| **Migrate to JetBrains Compose Multiplatform dependencies** | High | Low | β
|
-| **iOS CI gate (compile-only validation)** | High | Medium | β
|
-| **Commonize utilities** (`formatString`, `SfppHasher`, `CryptoCodec`, `CommonUri`) | High | Medium | β
|
-| **Centralize metric formatting** (`MetricFormatter`) | Medium | Low | β
|
-
-## Active Work
-
-### Desktop Feature Completion (Phase 4)
-
-**Objective:** Complete desktop wiring for all features and ensure full integration.
-
-**Current State (March 2026):**
-- β
**Settings:** ~35 screens with real configuration, including theme/about parity and desktop language picker support
-- β
**Nodes:** Adaptive list-detail with node management
-- β
**Messaging:** Adaptive contacts with message view + send
-- β
**Connections:** Dynamic discovery of platform-supported transports (TCP, Serial/USB, BLE)
-- β **Map:** Placeholder only, needs MapLibre or alternative
-- β οΈ **Firmware:** Fully KMP (Unified OTA + native Secure DFU + USB/UF2); desktop is first-class target
-- β οΈ **Intro:** Onboarding flow (may not apply to desktop)
-
-**Implementation Steps:**
-
-1. **Tier 1: Core Wiring (Essential)**
- - Complete Map integration (MapLibre or equivalent)
- - Verify all features accessible via navigation
- - Test navigation flows end-to-end
-2. **Tier 2: Polish (High Priority)**
- - Additional desktop-specific settings polish
- - β
**Keyboard shortcuts** via `onPreviewKeyEvent` (MenuBar removed)
- - **Adaptive density & multitasking optimizations** (2026 Desktop Guidelines)
- - Window management
- - State persistence
-3. **Tier 3: Advanced (Nice-to-have)**
- - Performance optimization
- - Advanced map features
- - Theme customization
- - Multi-window support
-
-| Transport | Platform | Status |
-|---|---|---|
-| TCP | Desktop (JVM) | β
Done β shared `StreamFrameCodec` + `TcpTransport` in `core:network` |
-| Serial/USB | Desktop (JVM) | β
Done β jSerialComm |
-| MQTT | All (KMP) | β
Completed β KMQTT in commonMain |
-| BLE | All (KMP) | β
Done β Kable in `commonMain` (`BleRadioTransport`) |
-
-### Desktop Feature Gaps
-
-| Feature | Status |
-|---|---|
-| Settings | β
~35 real screens (fully shared); `DeviceConfig`, `PositionConfig`, `SecurityConfig`, `ExternalNotificationConfig` fully unified into `commonMain` |
-| Node list | β
Adaptive list-detail with real `NodeDetailContent` |
-| Messaging | β
Adaptive contacts with real message view + send |
-| Connections | β
Unified shared UI with dynamic transport detection |
-| Metrics logs | β
TracerouteLog, NeighborInfoLog, HostMetricsLog |
-| Map | β Needs MapLibre or equivalent |
-| QR Generation | β
Pure KMP generation via `qrcode-kotlin` |
-| Charts | β
Vico KMP charts wired in commonMain (Device, Environment, Signal, Power, Pax) |
-| Debug Panel | β
Real screen (mesh log viewer via shared `DebugViewModel`) |
-| Notifications | β
Desktop native notifications with system tray icon support |
-| MenuBar | β
Removed β replaced with `onPreviewKeyEvent` keyboard shortcuts (βQ, β,, ββ§T, β1-4, β/) |
-| About | β
Shared `commonMain` screen (AboutLibraries KMP `produceLibraries` + per-platform JSON) |
-| Packaging | β
Done β Native distribution pipeline in CI (DMG, MSI, DEB). Windows `upgradeUuid` set; macOS signing/notarization wired behind `SIGN_MACOS` env flag; desktop build attestation in release CI. Flatpak packaging maintained externally at [vidplace7/org.meshtastic.desktop](https://github.com/vidplace7/org.meshtastic.desktop) (includes AppStream metainfo, `.desktop` entry, and JBR bundling); see [PR #4807](https://github.com/meshtastic/Meshtastic-Android/pull/4807) for `flatpakGradleGenerator` integration |
-
-## Near-Term Priorities (30 days)
-
-1. **Evaluate KMP-native testing tools** β β
**Done:** Fully evaluated and integrated `Mokkery`, `Turbine`, and `Kotest` across the KMP modules. `mockk` has been successfully replaced, enabling property-based and Flow testing in `commonTest` for iOS readiness.
-2. **Desktop Map Integration** β Address the major Desktop feature gap by implementing a raster map view using [**MapComposeMP**](https://github.com/p-lr/MapComposeMP).
- - Implement Desktop providers for the 3 decomposed map contracts: `MapViewProvider` (main map), `NodeTrackMapProvider` (per-node track overlay for `PositionLogScreen`), and `TracerouteMapProvider` (traceroute visualization).
- - Implement a **Web Mercator Projection** helper in `feature:map/commonMain` to translate GPS coordinates to the 2D image plane.
- - Leverage the existing `BaseMapViewModel` contract and `TracerouteNodeSelection` logic in `commonMain`.
-3. **Unify `MapViewModel`** β Collapse the remaining Google and F-Droid specific `MapViewModel` classes in the `:app` module into a single `commonMain` implementation by isolating platform-specific settings (styles, tile sources) behind a repository interface. The `MapViewProvider` interface has been simplified (track/traceroute rendering extracted to dedicated providers), reducing the surface area of this unification.
-4. **iOS CI gate** β β
**Done:** added `iosArm64()`/`iosSimulatorArm64()` to convention plugins and CI. `commonMain` successfully compiles on iOS.
-
-## Medium-Term Priorities (60 days)
-
-1. **iOS proof target** β β
**Done (Stubbing):** Stubbed iOS target implementations (`NoopStubs.kt` equivalent) to successfully pass compile-time checks. **Next:** Setup an Xcode skeleton project and launch the iOS app.
-2. **Migrate to Navigation 3 Scene-based architecture** β leverage the first stable release of Nav 3 to support multi-pane layouts. **Investigate 3-pane "Power User" scenes** (e.g., Node List + Detail + Map/Charts) on Large (1200dp) and Extra-large (1600dp) displays (Android 16 QPR3).
-3. **`core:api` contract split** β separate transport-neutral service contracts from the Android AIDL packaging to support iOS/Desktop service layers.
-
-## Longer-Term (90+ days)
-
-1. **Platform-Native UI Interop** β
- - **iOS Maps & Camera:** Implement `MapLibre` or `MKMapView` via Compose Multiplatform's `UIKitView`. Leverage `AVCaptureSession` wrapped in `UIKitView` to fulfill the `LocalBarcodeScannerProvider` contract.
- - **Web (wasmJs) Integrations:** Leverage `HtmlView` to embed raw DOM elements (e.g., `<video>`, `<iframe>`, or canvas-based maps) directly into the Compose UI tree while binding the root app via `CanvasBasedWindow`.
-2. **Module maturity dashboard** β living inventory of per-module KMP readiness.
-3. **Shared UI vs Shared Logic split** β If the iOS target utilizes native SwiftUI instead of Compose Multiplatform, evaluate splitting feature modules into pure `sharedLogic` (business rules, ViewModels) and `sharedUI` (Compose Multiplatform) to prevent dragging Compose dependencies into pure native iOS apps.
-
-## Design Principles
-
-1. **Solve in `commonMain` first.** If it doesn't need platform APIs, it belongs in `commonMain`.
-2. **Interfaces in `commonMain`, implementations per-target.** The repository pattern is established β extend it. Prefer dependency injection (Koin) with interfaces over `expect`/`actual` declarations whenever possible to keep architecture decoupled and highly testable.
-3. **UI Interop Strategies.** When a Compose Multiplatform equivalent doesn't exist (e.g., Maps, Camera), use standard interop APIs rather than extracting the entire screen to native code. Use `AndroidView` for Android, `UIKitView` for iOS, `SwingPanel` for JVM/Desktop, and `HtmlView` for Web (`wasmJs`). Always wrap these in a shared `commonMain` interface contract (like `LocalBarcodeScannerProvider`).
-4. **Stubs are a valid first implementation.** Every target starts with no-op stubs, then graduates to real implementations.
-5. **Feature modules stay target-agnostic in `commonMain`.** Platform UI goes in platform source sets. Keep the UI layer dumb and rely on shared ViewModels (Unidirectional Data Flow) to drive state.
-6. **Transport is a pluggable adapter.** BLE, serial, TCP, MQTT all implement `RadioTransport` and are orchestrated by a shared `RadioInterfaceService`.
-7. **CI validates every target.** If a module declares `jvm()`, CI compiles it. No exceptions. Run tests on appropriate host runners (macOS for iOS, Linux for JVM/Android) to catch platform regressions.
-8. **Test in `commonTest` first.** ViewModel and business logic tests belong in `commonTest` so every target runs them. Use shared `core:testing` utilities to minimize duplication.
-9. **Zero Platform Leaks.** Never import `java.*` or `android.*` inside `commonMain`. Use KMP-native alternatives like `kotlinx-datetime` and `Okio`.
diff --git a/feature/connections/detekt-baseline.xml b/feature/connections/detekt-baseline.xml
index 3ba42aec53..57ac7934a6 100644
--- a/feature/connections/detekt-baseline.xml
+++ b/feature/connections/detekt-baseline.xml
@@ -3,5 +3,11 @@
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>Kdoc:TcpDiscoveryHelpers.kt:/** * Shared helpers for TCP device discovery logic used by both [CommonGetDiscoveredDevicesUseCase] and the * Android-specific variant. */</ID>
+ <ID>PreviewPublic:ConnectionsPreviews.kt:@PreviewLightDark @Composable fun ConnectingDeviceInfoPreview</ID>
+ <ID>PreviewPublic:ConnectionsPreviews.kt:@PreviewLightDark @Composable fun DeviceListItemPreview</ID>
+ <ID>PreviewPublic:ConnectionsPreviews.kt:@PreviewLightDark @Composable fun DeviceSectionHeaderPreview</ID>
+ <ID>PreviewPublic:ConnectionsPreviews.kt:@PreviewLightDark @Composable fun DisconnectButtonPreview</ID>
+ <ID>PreviewPublic:ConnectionsPreviews.kt:@PreviewLightDark @Composable fun EmptyStateContentPreview</ID>
+ <ID>PreviewPublic:ConnectionsPreviews.kt:@PreviewLightDark @Composable fun TransportFilterChipsPreview</ID>
</CurrentIssues>
</SmellBaseline>
diff --git a/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/component/ConnectionsPreviews.kt b/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/component/ConnectionsPreviews.kt
new file mode 100644
index 0000000000..17b4934e05
--- /dev/null
+++ b/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/component/ConnectionsPreviews.kt
@@ -0,0 +1,86 @@
+/*
+ * Copyright (c) 2026 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package org.meshtastic.feature.connections.component
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.tooling.preview.PreviewLightDark
+import org.meshtastic.core.model.ConnectionState
+import org.meshtastic.core.ui.icon.MeshtasticIcons
+import org.meshtastic.core.ui.icon.Search
+import org.meshtastic.core.ui.theme.AppTheme
+import org.meshtastic.core.ui.viewmodel.ConnectionStatus
+import org.meshtastic.feature.connections.model.DeviceListEntry
+import org.meshtastic.feature.connections.ui.components.ConnectingDeviceInfo
+import org.meshtastic.feature.connections.ui.components.DeviceListItem
+import org.meshtastic.feature.connections.ui.components.DeviceSectionHeader
+import org.meshtastic.feature.connections.ui.components.DisconnectButton
+import org.meshtastic.feature.connections.ui.components.EmptyStateContent
+import org.meshtastic.feature.connections.ui.components.TransportFilterChips
+
+@PreviewLightDark
+@Composable
+fun DeviceListItemPreview() {
+ val device = DeviceListEntry.Tcp(name = "Meshtastic_abcd", fullAddress = "s192.168.1.100")
+ AppTheme { DeviceListItem(connectionState = ConnectionState.Disconnected, device = device, onSelect = {}) }
+}
+
+@PreviewLightDark
+@Composable
+fun DisconnectButtonPreview() {
+ AppTheme { DisconnectButton(onClick = {}) }
+}
+
+@PreviewLightDark
+@Composable
+fun ConnectingDeviceInfoPreview() {
+ AppTheme {
+ ConnectingDeviceInfo(
+ deviceName = "Meshtastic_abcd",
+ deviceAddress = "AA:BB:CC:DD:EE:FF",
+ connectionStatus = ConnectionStatus.CONNECTING,
+ connectionProgress = "Discovering services...",
+ onClickDisconnect = {},
+ )
+ }
+}
+
+@PreviewLightDark
+@Composable
+fun EmptyStateContentPreview() {
+ AppTheme { EmptyStateContent(text = "No devices found", imageVector = MeshtasticIcons.Search) }
+}
+
+@PreviewLightDark
+@Composable
+fun DeviceSectionHeaderPreview() {
+ AppTheme { DeviceSectionHeader(title = "Bluetooth Devices", showProgress = true) }
+}
+
+@PreviewLightDark
+@Composable
+fun TransportFilterChipsPreview() {
+ AppTheme {
+ TransportFilterChips(
+ showBle = true,
+ showNetwork = true,
+ showUsb = false,
+ onToggleBle = {},
+ onToggleNetwork = {},
+ onToggleUsb = {},
+ )
+ }
+}
diff --git a/feature/firmware/detekt-baseline.xml b/feature/firmware/detekt-baseline.xml
index 8e8a959a0d..71ba09da40 100644
--- a/feature/firmware/detekt-baseline.xml
+++ b/feature/firmware/detekt-baseline.xml
@@ -3,12 +3,22 @@
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>Kdoc:LegacyDfuTransport.kt:LegacyDfuTransport$/** * Stream [firmware] to the Packet characteristic, awaiting a [LegacyDfuResponse.PacketReceipt] every * [PRN_INTERVAL_PACKETS] packets and verifying the bytes-received count. * * Watches the connection state in parallel with the write loop; if the link drops mid-stream we cancel the write * coroutine and surface a [DfuException.ConnectionFailed] immediately rather than waiting indefinitely for a write * that will never complete. */</ID>
+ <ID>ModifierMissing:FirmwareUpdateScreen.kt:@Composable internal fun CheckingState</ID>
+ <ID>ModifierMissing:FirmwareUpdateScreen.kt:@Composable internal fun DisclaimerDialog</ID>
+ <ID>ModifierMissing:FirmwareUpdateScreen.kt:@Composable internal fun ErrorState</ID>
+ <ID>ModifierMissing:FirmwareUpdateScreen.kt:@Composable internal fun SuccessState</ID>
+ <ID>ModifierMissing:FirmwareUpdateScreen.kt:@Composable internal fun VerifyingState</ID>
<ID>MultipleEmitters:FirmwareUpdateScreen.kt:@Composable @Suppress("LongMethod") private fun ReadyState</ID>
+ <ID>MultipleEmitters:FirmwareUpdateScreen.kt:@Composable internal fun CheckingState</ID>
+ <ID>MultipleEmitters:FirmwareUpdateScreen.kt:@Composable internal fun ErrorState</ID>
+ <ID>MultipleEmitters:FirmwareUpdateScreen.kt:@Composable internal fun VerifyingState</ID>
<ID>MultipleEmitters:FirmwareUpdateScreen.kt:@Composable private fun AwaitingFileSaveState</ID>
- <ID>MultipleEmitters:FirmwareUpdateScreen.kt:@Composable private fun CheckingState</ID>
- <ID>MultipleEmitters:FirmwareUpdateScreen.kt:@Composable private fun ErrorState</ID>
<ID>MultipleEmitters:FirmwareUpdateScreen.kt:@Composable private fun VerificationFailedState</ID>
- <ID>MultipleEmitters:FirmwareUpdateScreen.kt:@Composable private fun VerifyingState</ID>
+ <ID>PreviewPublic:FirmwarePreviews.kt:@PreviewLightDark @Composable fun CheckingStatePreview</ID>
+ <ID>PreviewPublic:FirmwarePreviews.kt:@PreviewLightDark @Composable fun DisclaimerDialogPreview</ID>
+ <ID>PreviewPublic:FirmwarePreviews.kt:@PreviewLightDark @Composable fun ErrorStatePreview</ID>
+ <ID>PreviewPublic:FirmwarePreviews.kt:@PreviewLightDark @Composable fun SuccessStatePreview</ID>
+ <ID>PreviewPublic:FirmwarePreviews.kt:@PreviewLightDark @Composable fun VerifyingStatePreview</ID>
<ID>TooGenericExceptionCaught:FirmwareUpdateViewModel.kt:FirmwareUpdateViewModel$e: Exception</ID>
</CurrentIssues>
</SmellBaseline>
diff --git a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwarePreviews.kt b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwarePreviews.kt
new file mode 100644
index 0000000000..47e8bbc28a
--- /dev/null
+++ b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwarePreviews.kt
@@ -0,0 +1,100 @@
+/*
+ * Copyright (c) 2026 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package org.meshtastic.feature.firmware
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Surface
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.PreviewLightDark
+import androidx.compose.ui.unit.dp
+import org.meshtastic.core.resources.UiText
+import org.meshtastic.core.ui.theme.AppTheme
+
+@PreviewLightDark
+@Composable
+fun VerifyingStatePreview() {
+ AppTheme {
+ Surface {
+ Column(
+ modifier = Modifier.fillMaxSize().padding(24.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center,
+ ) {
+ VerifyingState()
+ }
+ }
+ }
+}
+
+@PreviewLightDark
+@Composable
+fun CheckingStatePreview() {
+ AppTheme {
+ Surface {
+ Column(
+ modifier = Modifier.fillMaxSize().padding(24.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center,
+ ) {
+ CheckingState()
+ }
+ }
+ }
+}
+
+@PreviewLightDark
+@Composable
+fun ErrorStatePreview() {
+ AppTheme {
+ Surface {
+ Column(
+ modifier = Modifier.fillMaxSize().padding(24.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center,
+ ) {
+ ErrorState(error = UiText.DynamicString("Connection lost"), onRetry = {})
+ }
+ }
+ }
+}
+
+@PreviewLightDark
+@Composable
+fun SuccessStatePreview() {
+ AppTheme {
+ Surface {
+ Column(
+ modifier = Modifier.fillMaxSize().padding(24.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center,
+ ) {
+ SuccessState(onDone = {})
+ }
+ }
+ }
+}
+
+@PreviewLightDark
+@Composable
+fun DisclaimerDialogPreview() {
+ AppTheme { Surface { DisclaimerDialog(updateMethod = FirmwareUpdateMethod.Ble, onDismiss = {}, onConfirm = {}) } }
+}
diff --git a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateScreen.kt b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateScreen.kt
index 80f9dee112..e63ab43389 100644
--- a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateScreen.kt
+++ b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateScreen.kt
@@ -336,7 +336,7 @@ private fun FirmwareUpdateContent(
}
@Composable
-private fun VerifyingState() {
+internal fun VerifyingState() {
CircularProgressIndicator(modifier = Modifier.size(64.dp))
Spacer(Modifier.height(24.dp))
Text(stringResource(Res.string.firmware_update_verifying), style = MaterialTheme.typography.titleMedium)
@@ -351,7 +351,7 @@ private fun VerifyingState() {
}
@Composable
-private fun CheckingState() {
+internal fun CheckingState() {
CircularProgressIndicator(modifier = Modifier.size(64.dp))
Spacer(Modifier.height(24.dp))
Text(stringResource(Res.string.firmware_update_checking), style = MaterialTheme.typography.bodyLarge)
@@ -446,7 +446,7 @@ private fun ReadyState(
}
@Composable
-private fun DisclaimerDialog(updateMethod: FirmwareUpdateMethod, onDismiss: () -> Unit, onConfirm: () -> Unit) {
+internal fun DisclaimerDialog(updateMethod: FirmwareUpdateMethod, onDismiss: () -> Unit, onConfirm: () -> Unit) {
MeshtasticDialog(
onDismiss = onDismiss,
title = stringResource(Res.string.firmware_update_disclaimer_title),
@@ -832,7 +832,7 @@ private fun VerificationFailedState(onRetry: () -> Unit, onIgnore: () -> Unit) {
}
@Composable
-private fun ErrorState(error: UiText, onRetry: () -> Unit) {
+internal fun ErrorState(error: UiText, onRetry: () -> Unit) {
Icon(
MeshtasticIcons.Dangerous,
contentDescription = null,
@@ -855,7 +855,7 @@ private fun ErrorState(error: UiText, onRetry: () -> Unit) {
}
@Composable
-private fun SuccessState(onDone: () -> Unit) {
+internal fun SuccessState(onDone: () -> Unit) {
val haptic = LocalHapticFeedback.current
LaunchedEffect(Unit) { haptic.performHapticFeedback(HapticFeedbackType.LongPress) }
diff --git a/feature/intro/detekt-baseline.xml b/feature/intro/detekt-baseline.xml
index f31f771653..23792d7491 100644
--- a/feature/intro/detekt-baseline.xml
+++ b/feature/intro/detekt-baseline.xml
@@ -4,5 +4,6 @@
<CurrentIssues>
<ID>ComposableParamOrder:PermissionScreenLayout.kt:@Composable internal fun PermissionScreenLayout</ID>
<ID>ParameterNaming:WelcomeScreen.kt:onGetStarted: () -> Unit</ID>
+ <ID>PreviewPublic:WelcomeScreen.kt:@Preview @Composable fun WelcomeScreenPreview</ID>
</CurrentIssues>
</SmellBaseline>
diff --git a/feature/intro/src/commonMain/kotlin/org/meshtastic/feature/intro/WelcomeScreen.kt b/feature/intro/src/commonMain/kotlin/org/meshtastic/feature/intro/WelcomeScreen.kt
index c3f66e4b24..8cca4949fe 100644
--- a/feature/intro/src/commonMain/kotlin/org/meshtastic/feature/intro/WelcomeScreen.kt
+++ b/feature/intro/src/commonMain/kotlin/org/meshtastic/feature/intro/WelcomeScreen.kt
@@ -116,8 +116,9 @@ internal fun WelcomeScreen(onGetStarted: () -> Unit) {
}
}
+@Suppress("PreviewPublic")
@PreviewLightDark
@Composable
-private fun WelcomeScreenPreview() {
+fun WelcomeScreenPreview() {
AppTheme { Surface { WelcomeScreen(onGetStarted = {}) } }
}
diff --git a/feature/messaging/detekt-baseline.xml b/feature/messaging/detekt-baseline.xml
index 005614b429..2986c92922 100644
--- a/feature/messaging/detekt-baseline.xml
+++ b/feature/messaging/detekt-baseline.xml
@@ -31,6 +31,10 @@
<ID>ParameterNaming:MessageScreenComponents.kt:onToggleFilteringDisabled: () -> Unit = {}</ID>
<ID>ParameterNaming:MessageScreenComponents.kt:onToggleShowFiltered: () -> Unit</ID>
<ID>ParameterNaming:MessageScreenComponents.kt:onToggleShowFiltered: () -> Unit = {}</ID>
+ <ID>PreviewPublic:Message.kt:@PreviewLightDark @Composable fun MessageInputPreview</ID>
+ <ID>PreviewPublic:QuickChatPreviews.kt:@PreviewLightDark @Composable fun EditQuickChatDialogPreview</ID>
+ <ID>PreviewPublic:QuickChatPreviews.kt:@PreviewLightDark @Composable fun QuickChatItemPreview</ID>
+ <ID>PreviewPublic:ReactionPreviews.kt:@PreviewLightDark @Composable fun ReactionItemPreview</ID>
<ID>ViewModelForwarding:AdaptiveContactsScreen.kt:ContactsScreen( onNavigateToShare = { backStack.add(ChannelsRoute.ChannelsGraph) }, sharedContactRequested = sharedContactRequested, requestChannelSet = requestChannelSet, onHandleDeepLink = onHandleDeepLink, onClearSharedContactRequested = onClearSharedContactRequested, onClearRequestChannelUrl = onClearRequestChannelUrl, viewModel = contactsViewModel, onClickNodeChip = { backStack.add(NodesRoute.NodeDetail(it)) }, onNavigateToMessages = { contactKey -> backStack.add(ContactsRoute.Messages(contactKey)) }, onNavigateToNodeDetails = { backStack.add(NodesRoute.NodeDetail(it)) }, scrollToTopEvents = scrollToTopEvents, activeContactKey = null, )</ID>
</CurrentIssues>
</SmellBaseline>
diff --git a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/Message.kt b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/Message.kt
index e88a73077c..89ac0ef45b 100644
--- a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/Message.kt
+++ b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/Message.kt
@@ -521,7 +521,7 @@ private fun MessageInput(
@PreviewLightDark
@Composable
-private fun MessageInputPreview() {
+fun MessageInputPreview() {
AppTheme {
Surface {
Column(modifier = Modifier.padding(8.dp)) {
diff --git a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/QuickChatPreviews.kt b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/QuickChatPreviews.kt
index 2cfe307cf0..56776f1438 100644
--- a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/QuickChatPreviews.kt
+++ b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/QuickChatPreviews.kt
@@ -23,13 +23,13 @@ import org.meshtastic.core.ui.theme.AppTheme
@PreviewLightDark
@Composable
-private fun QuickChatItemPreview() {
+fun QuickChatItemPreview() {
AppTheme { QuickChatItem(action = QuickChatAction(name = "TST", message = "Test", position = 0)) }
}
@PreviewLightDark
@Composable
-private fun EditQuickChatDialogPreview() {
+fun EditQuickChatDialogPreview() {
AppTheme {
EditQuickChatDialog(
action = QuickChatAction(name = "TST", message = "Test", position = 0),
diff --git a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/ReactionPreviews.kt b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/ReactionPreviews.kt
index 968ec13f48..aca71e2932 100644
--- a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/ReactionPreviews.kt
+++ b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/ReactionPreviews.kt
@@ -29,11 +29,11 @@ import org.meshtastic.proto.User
@PreviewLightDark
@Composable
-private fun ReactionItemPreview() {
+fun ReactionItemPreview() {
AppTheme {
Column(modifier = Modifier.background(MaterialTheme.colorScheme.background)) {
- ReactionItem(emoji = "\uD83D\uDE42")
- ReactionItem(emoji = "\uD83D\uDE42", emojiCount = 2)
+ ReactionItem(emoji = "+1")
+ ReactionItem(emoji = "+1", emojiCount = 2)
AddReactionButton()
}
}
diff --git a/feature/node/detekt-baseline.xml b/feature/node/detekt-baseline.xml
index 787decdf52..6fb5a182df 100644
--- a/feature/node/detekt-baseline.xml
+++ b/feature/node/detekt-baseline.xml
@@ -66,6 +66,18 @@
<ID>ParameterNaming:SignalMetrics.kt:onPointSelected: (Double) -> Unit</ID>
<ID>ParameterNaming:TimeFrameSelector.kt:onTimeFrameSelected: (TimeFrame) -> Unit</ID>
<ID>ParameterNaming:TracerouteChart.kt:onPointSelected: (Double) -> Unit</ID>
+ <ID>PreviewPublic:CommonCharts.kt:@PreviewLightDark @Composable fun LegendPreview</ID>
+ <ID>PreviewPublic:DeviceMetrics.kt:@PreviewLightDark @Suppress("detekt:MagicNumber") // Compose preview with fake data @Composable fun DeviceMetricsCardPreview</ID>
+ <ID>PreviewPublic:EnvironmentMetrics.kt:@PreviewLightDark @Suppress("MagicNumber") // Compose preview with fake data @Composable fun PreviewEnvironmentMetricsContent</ID>
+ <ID>PreviewPublic:NodeDetailComponentPreviews.kt:@PreviewLightDark @Composable fun DeviceActionsLocalPreview</ID>
+ <ID>PreviewPublic:NodeDetailComponentPreviews.kt:@PreviewLightDark @Composable fun DeviceActionsRemotePreview</ID>
+ <ID>PreviewPublic:NodeDetailComponentPreviews.kt:@PreviewLightDark @Composable fun NodeDetailsSectionPreview</ID>
+ <ID>PreviewPublic:NodeDetailComponentPreviews.kt:@PreviewLightDark @Composable fun PositionInlineContentPreview</ID>
+ <ID>PreviewPublic:NodeDetailComponentPreviews.kt:@PreviewLightDark @Composable fun TelemetricActionsSectionEmptyPreview</ID>
+ <ID>PreviewPublic:NodeDetailComponentPreviews.kt:@PreviewLightDark @Composable fun TelemetricActionsSectionPreview</ID>
+ <ID>PreviewPublic:NodeDetailPreviews.kt:@PreviewLightDark @Composable fun NodeDetailContentLoadingPreview</ID>
+ <ID>PreviewPublic:NodeDetailPreviews.kt:@PreviewLightDark @Composable fun NodeDetailContentLocalPreview</ID>
+ <ID>PreviewPublic:NodeDetailPreviews.kt:@PreviewLightDark @Composable fun NodeDetailContentRemotePreview</ID>
<ID>TooGenericExceptionCaught:MetricsViewModel.kt:MetricsViewModel$e: Exception</ID>
<ID>TooGenericExceptionCaught:NodeManagementActions.kt:NodeManagementActions$ex: Exception</ID>
<ID>ViewModelForwarding:NodeDetailScreens.kt:NodeDetailScaffold( modifier = modifier, uiState = uiState, viewModel = viewModel, navigateToMessages = navigateToMessages, onNavigate = onNavigate, onNavigateUp = onNavigateUp, compassViewModel = compassViewModel, )</ID>
diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeDetailComponentPreviews.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeDetailComponentPreviews.kt
index 4d9287becc..1701f6f7fc 100644
--- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeDetailComponentPreviews.kt
+++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeDetailComponentPreviews.kt
@@ -38,7 +38,7 @@ private val previewData = NodePreviewParameterProvider()
@PreviewLightDark
@Composable
-private fun DeviceActionsRemotePreview() {
+fun DeviceActionsRemotePreview() {
val node = previewData.mickeyMouse
AppTheme {
Surface {
@@ -65,7 +65,7 @@ private fun DeviceActionsRemotePreview() {
@PreviewLightDark
@Composable
-private fun DeviceActionsLocalPreview() {
+fun DeviceActionsLocalPreview() {
val node = previewData.mickeyMouse
AppTheme {
Surface {
@@ -90,7 +90,7 @@ private fun DeviceActionsLocalPreview() {
@PreviewLightDark
@Composable
-private fun TelemetricActionsSectionPreview() {
+fun TelemetricActionsSectionPreview() {
val node = previewData.mickeyMouse
AppTheme {
Surface {
@@ -118,7 +118,7 @@ private fun TelemetricActionsSectionPreview() {
@PreviewLightDark
@Composable
-private fun TelemetricActionsSectionEmptyPreview() {
+fun TelemetricActionsSectionEmptyPreview() {
val node = previewData.minnieMouse
AppTheme {
Surface {
@@ -142,7 +142,7 @@ private fun TelemetricActionsSectionEmptyPreview() {
@PreviewLightDark
@Composable
-private fun PositionInlineContentPreview() {
+fun PositionInlineContentPreview() {
val node = previewData.mickeyMouse
AppTheme {
Surface {
@@ -162,7 +162,7 @@ private fun PositionInlineContentPreview() {
@PreviewLightDark
@Composable
-private fun NodeDetailsSectionPreview() {
+fun NodeDetailsSectionPreview() {
val node = previewData.mickeyMouse
AppTheme { Surface { NodeDetailsSection(node = node) } }
}
diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/NodeDetailPreviews.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/NodeDetailPreviews.kt
index caa68b1067..432bbe1c30 100644
--- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/NodeDetailPreviews.kt
+++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/NodeDetailPreviews.kt
@@ -38,7 +38,7 @@ private val previewData = NodePreviewParameterProvider()
@PreviewLightDark
@Composable
-private fun NodeDetailContentRemotePreview() {
+fun NodeDetailContentRemotePreview() {
val node = previewData.mickeyMouse
AppTheme {
Surface {
@@ -67,7 +67,7 @@ private fun NodeDetailContentRemotePreview() {
@PreviewLightDark
@Composable
-private fun NodeDetailContentLocalPreview() {
+fun NodeDetailContentLocalPreview() {
val node = previewData.mickeyMouse
AppTheme {
Surface {
@@ -89,7 +89,7 @@ private fun NodeDetailContentLocalPreview() {
@PreviewLightDark
@Composable
-private fun NodeDetailContentLoadingPreview() {
+fun NodeDetailContentLoadingPreview() {
AppTheme {
Surface {
NodeDetailContent(
diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/CommonCharts.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/CommonCharts.kt
index 278ea3f127..43ceebb46b 100644
--- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/CommonCharts.kt
+++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/CommonCharts.kt
@@ -253,9 +253,8 @@ fun MetricIndicator(color: Color, modifier: Modifier = Modifier) {
}
@PreviewLightDark
-@Suppress("unused") // Compose preview
@Composable
-private fun LegendPreview() {
+fun LegendPreview() {
val data =
listOf(
LegendData(nameRes = Res.string.rssi, color = Color.Red, isLine = true),
diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/DeviceMetrics.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/DeviceMetrics.kt
index b384c6e57c..4caa71bf7b 100644
--- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/DeviceMetrics.kt
+++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/DeviceMetrics.kt
@@ -476,8 +476,8 @@ private fun DeviceMetricsCard(telemetry: Telemetry, isSelected: Boolean, onClick
@PreviewLightDark
@Suppress("detekt:MagicNumber") // Compose preview with fake data
@Composable
-private fun DeviceMetricsCardPreview() {
- val now = nowSeconds.toInt()
+fun DeviceMetricsCardPreview() {
+ val now = 1700000000
val telemetry =
Telemetry(
time = now,
diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/EnvironmentMetrics.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/EnvironmentMetrics.kt
index 45eb43500a..f843eefbe3 100644
--- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/EnvironmentMetrics.kt
+++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/EnvironmentMetrics.kt
@@ -44,7 +44,6 @@ import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.common.util.DateFormatter
import org.meshtastic.core.common.util.MetricFormatter
import org.meshtastic.core.common.util.formatString
-import org.meshtastic.core.common.util.nowSeconds
import org.meshtastic.core.model.TelemetryType
import org.meshtastic.core.model.util.TimeConstants.MS_PER_SEC
import org.meshtastic.core.resources.Res
@@ -541,7 +540,7 @@ private fun EnvironmentMetricsContent(telemetry: Telemetry, environmentDisplayFa
@PreviewLightDark
@Suppress("MagicNumber") // Compose preview with fake data
@Composable
-private fun PreviewEnvironmentMetricsContent() {
+fun PreviewEnvironmentMetricsContent() {
val fakeEnvMetrics =
org.meshtastic.proto.EnvironmentMetrics(
temperature = 22.5f,
@@ -563,6 +562,6 @@ private fun PreviewEnvironmentMetricsContent() {
rainfall_1h = 1.5f,
rainfall_24h = 12.3f,
)
- val fakeTelemetry = Telemetry(time = nowSeconds.toInt(), environment_metrics = fakeEnvMetrics)
+ val fakeTelemetry = Telemetry(time = 1700000000, environment_metrics = fakeEnvMetrics)
AppTheme { Surface { EnvironmentMetricsContent(telemetry = fakeTelemetry, environmentDisplayFahrenheit = false) } }
}
diff --git a/feature/settings/detekt-baseline.xml b/feature/settings/detekt-baseline.xml
index e572be9c3e..a6898f3650 100644
--- a/feature/settings/detekt-baseline.xml
+++ b/feature/settings/detekt-baseline.xml
@@ -48,7 +48,6 @@
<ID>ModifierMissing:ChannelConfigScreen.kt:@Composable fun ChannelConfigScreen</ID>
<ID>ModifierMissing:ChannelScreen.kt:@Composable @Suppress("LongMethod") fun ChannelScreen</ID>
<ID>ModifierMissing:CleanNodeDatabaseScreen.kt:@Composable fun CleanNodeDatabaseScreen</ID>
- <ID>ModifierMissing:ContrastPickerDialog.kt:@Composable fun ContrastPickerDialog</ID>
<ID>ModifierMissing:Debug.kt:@Suppress("LongMethod") @Composable fun DebugScreen</ID>
<ID>ModifierMissing:DesktopSettingsScreen.kt:@Suppress("LongMethod") @Composable fun DesktopSettingsScreen</ID>
<ID>ModifierMissing:DetectionSensorConfigItemList.kt:@Composable fun DetectionSensorConfigScreen</ID>
@@ -101,6 +100,9 @@
<ID>ParameterNaming:PositionConfigScreen.android.kt:onLocationReceived: (Position) -> Unit</ID>
<ID>ParameterNaming:PositionConfigScreen.jvm.kt:onLocationReceived: (Position) -> Unit</ID>
<ID>ParameterNaming:PositionConfigScreen.kt:onLocationReceived: (Position) -> Unit</ID>
+ <ID>PreviewPublic:AppInfoSection.kt:@Preview(showBackground = true) @Composable fun AppInfoSectionPreview</ID>
+ <ID>PreviewPublic:AppearanceSection.kt:@Preview(showBackground = true) @Composable fun AppearanceSectionPreview</ID>
+ <ID>PreviewPublic:PersistenceSection.kt:@Preview(showBackground = true) @Composable fun PersistenceSectionPreview</ID>
<ID>ReturnCount:RadioConfigViewModel.kt:RadioConfigViewModel$private fun processPacketResponse</ID>
<ID>TooGenericExceptionCaught:DebugViewModel.kt:DebugViewModel$e: Exception</ID>
<ID>TooManyFunctions:RadioConfigViewModel.kt:RadioConfigViewModel : ViewModel</ID>
diff --git a/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/component/AppInfoSection.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/component/AppInfoSection.kt
index 9522c92922..90636e1d86 100644
--- a/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/component/AppInfoSection.kt
+++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/component/AppInfoSection.kt
@@ -160,7 +160,7 @@ private fun AppVersionButton(
@Preview(showBackground = true)
@Composable
-private fun AppInfoSectionPreview() {
+fun AppInfoSectionPreview() {
AppTheme {
AppInfoSection(
appVersionName = "2.5.0",
diff --git a/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/component/AppearanceSection.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/component/AppearanceSection.kt
index 586c4fbc89..44137580da 100644
--- a/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/component/AppearanceSection.kt
+++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/component/AppearanceSection.kt
@@ -79,6 +79,6 @@ fun AppearanceSection(onShowLanguagePicker: () -> Unit, onShowThemePicker: () ->
@Preview(showBackground = true)
@Composable
-private fun AppearanceSectionPreview() {
+fun AppearanceSectionPreview() {
AppTheme { AppearanceSection(onShowLanguagePicker = {}, onShowThemePicker = {}) }
}
diff --git a/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/component/PersistenceSection.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/component/PersistenceSection.kt
index 25fe82b47b..2c72a745e4 100644
--- a/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/component/PersistenceSection.kt
+++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/component/PersistenceSection.kt
@@ -111,6 +111,6 @@ fun PersistenceSection(
@Preview(showBackground = true)
@Composable
-private fun PersistenceSectionPreview() {
+fun PersistenceSectionPreview() {
AppTheme { PersistenceSection(cacheLimit = 100, onSetCacheLimit = {}, nodeShortName = "TEST", onExportData = {}) }
}
diff --git a/feature/wifi-provision/detekt-baseline.xml b/feature/wifi-provision/detekt-baseline.xml
index 3692f80fd9..2449b6221b 100644
--- a/feature/wifi-provision/detekt-baseline.xml
+++ b/feature/wifi-provision/detekt-baseline.xml
@@ -3,5 +3,13 @@
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>ModifierMissing:WifiProvisionScreen.kt:@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @Suppress("LongMethod") @Composable fun WifiProvisionScreen</ID>
+ <ID>PreviewPublic:WifiProvisionPreviews.kt:@PreviewLightDark @Composable fun ConnectedFailedPreview</ID>
+ <ID>PreviewPublic:WifiProvisionPreviews.kt:@PreviewLightDark @Composable fun ConnectedSuccessPreview</ID>
+ <ID>PreviewPublic:WifiProvisionPreviews.kt:@PreviewLightDark @Composable fun ConnectedWithNetworksPreview</ID>
+ <ID>PreviewPublic:WifiProvisionPreviews.kt:@PreviewLightDark @Composable fun DeviceFoundPreview</ID>
+ <ID>PreviewPublic:WifiProvisionPreviews.kt:@PreviewLightDark @Composable fun MpwrdDisclaimerBannerPreview</ID>
+ <ID>PreviewPublic:WifiProvisionPreviews.kt:@PreviewLightDark @Composable fun NetworkRowPreview</ID>
+ <ID>PreviewPublic:WifiProvisionPreviews.kt:@PreviewLightDark @Composable fun ProvisionStatusCardSuccessPreview</ID>
+ <ID>PreviewPublic:WifiProvisionPreviews.kt:@PreviewLightDark @Composable fun ScanningBlePreview</ID>
</CurrentIssues>
</SmellBaseline>
diff --git a/feature/wifi-provision/src/commonMain/kotlin/org/meshtastic/feature/wifiprovision/ui/WifiProvisionPreviews.kt b/feature/wifi-provision/src/commonMain/kotlin/org/meshtastic/feature/wifiprovision/ui/WifiProvisionPreviews.kt
index d74d1f615d..a342efc267 100644
--- a/feature/wifi-provision/src/commonMain/kotlin/org/meshtastic/feature/wifiprovision/ui/WifiProvisionPreviews.kt
+++ b/feature/wifi-provision/src/commonMain/kotlin/org/meshtastic/feature/wifiprovision/ui/WifiProvisionPreviews.kt
@@ -79,7 +79,7 @@ private val noOpProvision: (String, String, Boolean) -> Unit = { _, _, _ -> }
@PreviewLightDark
@Composable
-private fun ScanningBlePreview() {
+fun ScanningBlePreview() {
AppTheme { Surface(Modifier.fillMaxSize()) { ScanningBleContent() } }
}
@@ -89,7 +89,7 @@ private fun ScanningBlePreview() {
@PreviewLightDark
@Composable
-private fun DeviceFoundPreview() {
+fun DeviceFoundPreview() {
AppTheme {
Surface(Modifier.fillMaxSize()) {
DeviceFoundContent(deviceName = "mpwrd-nm-A1B2", onProceed = noOp, onCancel = noOp)
@@ -121,7 +121,7 @@ private fun ScanningNetworksPreview() {
@PreviewLightDark
@Composable
-private fun ConnectedWithNetworksPreview() {
+fun ConnectedWithNetworksPreview() {
AppTheme {
Surface(Modifier.fillMaxSize()) {
ConnectedContent(
@@ -197,7 +197,7 @@ private fun ConnectedProvisioningPreview() {
@PreviewLightDark
@Composable
-private fun ConnectedSuccessPreview() {
+fun ConnectedSuccessPreview() {
AppTheme {
Surface(Modifier.fillMaxSize()) {
ConnectedContent(
@@ -216,7 +216,7 @@ private fun ConnectedSuccessPreview() {
@PreviewLightDark
@Composable
-private fun ConnectedFailedPreview() {
+fun ConnectedFailedPreview() {
AppTheme {
Surface(Modifier.fillMaxSize()) {
ConnectedContent(
@@ -307,7 +307,7 @@ private fun ProvisionStatusCardProvisioningPreview() {
@PreviewLightDark
@Composable
-private fun ProvisionStatusCardSuccessPreview() {
+fun ProvisionStatusCardSuccessPreview() {
AppTheme {
Surface {
Column(modifier = Modifier.fillMaxWidth().padding(16.dp)) {
@@ -331,7 +331,7 @@ private fun ProvisionStatusCardFailedPreview() {
@PreviewLightDark
@Composable
-private fun NetworkRowPreview() {
+fun NetworkRowPreview() {
AppTheme {
Surface {
Column(modifier = Modifier.fillMaxWidth()) {
@@ -361,6 +361,6 @@ private fun NetworkRowLongSsidPreview() {
@PreviewLightDark
@Composable
-private fun MpwrdDisclaimerBannerPreview() {
+fun MpwrdDisclaimerBannerPreview() {
AppTheme { Surface { Column(modifier = Modifier.fillMaxWidth().padding(16.dp)) { MpwrdDisclaimerBanner() } } }
}
diff --git a/gradle.properties b/gradle.properties
index f98b9817f4..41bdd6f295 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -3,6 +3,7 @@ android.defaults.buildfeatures.resvalues=false
android.defaults.buildfeatures.shaders=false
# --- Compose ---
+android.experimental.enableScreenshotTest=true
compose.hot.reload=true
enableComposeCompilerMetrics=false
enableComposeCompilerReports=false
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 89443e66df..e8f8d93fd9 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -32,6 +32,9 @@ kotest = "6.1.11"
testRetry = "1.6.4"
turbine = "1.2.1"
+# Compose Screenshot Testing
+compose-screenshot = "0.0.1-alpha14"
+
# Compose Multiplatform
compose-multiplatform = "1.11.0-rc01"
compose-multiplatform-material3 = "1.11.0-alpha07"
@@ -242,6 +245,9 @@ usb-serial-android = { module = "com.github.mik3y:usb-serial-for-android", versi
vico-compose = { group = "com.patrykandpatrick.vico", name = "compose", version.ref = "vico" }
vico-compose-m3 = { group = "com.patrykandpatrick.vico", name = "compose-m3", version.ref = "vico" }
+# Screenshot Testing
+screenshot-validation-api = { group = "com.android.tools.screenshot", name = "screenshot-validation-api", version.ref = "compose-screenshot" }
+
# Build Logic
android-gradleApiPlugin = { module = "com.android.tools.build:gradle-api", version.ref = "agp" }
android-tools-common = { module = "com.android.tools:common", version = "32.2.1" }
@@ -270,6 +276,7 @@ jna = { module = "net.java.dev.jna:jna", version = "5.18.1" }
# Android
android-application = { id = "com.android.application", version.ref = "agp" }
android-kotlin-multiplatform-library = { id = "com.android.kotlin.multiplatform.library", version.ref = "agp" }
+compose-screenshot = { id = "com.android.compose.screenshot", version.ref = "compose-screenshot" }
# Jetbrains
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
diff --git a/screenshot-tests/build.gradle.kts b/screenshot-tests/build.gradle.kts
new file mode 100644
index 0000000000..c6198a8ea8
--- /dev/null
+++ b/screenshot-tests/build.gradle.kts
@@ -0,0 +1,105 @@
+/*
+ * Copyright (c) 2026 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+import com.android.build.api.dsl.LibraryExtension
+import org.gradle.testretry.TestRetryTaskExtension
+
+plugins {
+ alias(libs.plugins.meshtastic.android.library)
+ alias(libs.plugins.meshtastic.android.library.compose)
+ alias(libs.plugins.compose.screenshot)
+}
+
+configure<LibraryExtension> {
+ namespace = "org.meshtastic.screenshot.tests"
+
+ experimentalProperties["android.experimental.enableScreenshotTest"] = true
+
+ testOptions { screenshotTests { imageDifferenceThreshold = 0.0005f } }
+}
+
+// CST screenshot tests use a custom runner incompatible with test-retry
+tasks.withType<Test>().configureEach {
+ if (name.contains("ScreenshotTest", ignoreCase = true)) {
+ extensions.configure<TestRetryTaskExtension> { maxRetries.set(0) }
+ }
+}
+
+dependencies {
+ implementation(project(":core:ui"))
+ implementation(project(":core:resources"))
+ implementation(project(":core:model"))
+ implementation(project(":core:common"))
+ implementation(project(":feature:messaging"))
+ implementation(project(":feature:node"))
+ implementation(project(":feature:wifi-provision"))
+ implementation(project(":feature:connections"))
+ implementation(project(":feature:settings"))
+ implementation(project(":feature:firmware"))
+ implementation(project(":feature:intro"))
+
+ implementation(libs.compose.multiplatform.foundation)
+ implementation(libs.compose.multiplatform.material3)
+ implementation(libs.compose.multiplatform.runtime)
+ implementation(libs.compose.multiplatform.ui)
+
+ screenshotTestImplementation(libs.screenshot.validation.api)
+}
+
+tasks.register<Copy>("copyDocsScreenshots") {
+ description = "Copies selected reference screenshots to docs/screenshots/ for the docs pipeline."
+ group = "documentation"
+
+ val referenceDir = layout.projectDirectory.dir("src/screenshotTestDebug/reference")
+ val manifestFile = layout.projectDirectory.file("docs-screenshots-manifest.txt")
+ val outputDir = rootProject.layout.projectDirectory.dir("docs/screenshots")
+
+ from(referenceDir)
+ into(outputDir)
+
+ // Flatten directory structure: just the filename
+ eachFile { path = name }
+ duplicatesStrategy = DuplicatesStrategy.FAIL
+ includeEmptyDirs = false
+
+ doFirst {
+ val refDir = referenceDir.asFile
+ require(refDir.exists()) {
+ "Reference screenshot directory not found: ${refDir.absolutePath}. " +
+ "Run :screenshot-tests:updateDebugScreenshotTest first."
+ }
+ val file = manifestFile.asFile
+ require(file.exists()) {
+ "Screenshot manifest not found: ${file.absolutePath}. " +
+ "This file lists which reference screenshots to copy for the docs pipeline."
+ }
+ val patterns = file.readLines().map { it.trim() }.filter { it.isNotEmpty() && !it.startsWith("#") }
+ if (patterns.isEmpty()) {
+ logger.warn("Screenshot manifest is empty β no files will be copied: ${file.absolutePath}")
+ }
+ include(patterns)
+ }
+
+ doLast {
+ val copied = outputs.files.files.filter { it.isFile }
+ if (copied.isEmpty()) {
+ logger.warn(
+ "copyDocsScreenshots: manifest patterns matched no files in ${referenceDir.asFile.absolutePath}. " +
+ "Check pattern spelling in ${manifestFile.asFile.name}.",
+ )
+ }
+ }
+}
diff --git a/screenshot-tests/docs-screenshots-manifest.txt b/screenshot-tests/docs-screenshots-manifest.txt
new file mode 100644
index 0000000000..1e9df32bf8
--- /dev/null
+++ b/screenshot-tests/docs-screenshots-manifest.txt
@@ -0,0 +1,27 @@
+# Manifest of reference images to copy to docs/screenshots/ for the docs pipeline.
+# One glob pattern per line, matched against paths under src/screenshotTestDebug/reference/.
+# All tests use @PreviewLightDark β only light variants (_Light_) are included for docs.
+
+# Core UI components
+**/ComponentScreenshotTestsKt/Screenshot*_Light_*.png
+
+# Preference components
+**/PreferenceComponentScreenshotTestsKt/Screenshot*_Light_*.png
+
+# Alert dialogs
+**/AlertScreenshotTestsKt/Screenshot*_Light_*.png
+
+# Feature: Connections
+**/ConnectionsScreenshotTestsKt/Screenshot*_Light_*.png
+
+# Feature: Firmware
+**/FirmwareScreenshotTestsKt/Screenshot*_Light_*.png
+
+# Feature: Intro
+**/IntroScreenshotTestsKt/Screenshot*_Light_*.png
+
+# Feature: Settings
+**/SettingsScreenshotTestsKt/Screenshot*_Light_*.png
+
+# Feature: WiFi Provision
+**/WifiProvisionScreenshotTestsKt/Screenshot*_Light_*.png
diff --git a/screenshot-tests/src/main/AndroidManifest.xml b/screenshot-tests/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..8072ee00db
--- /dev/null
+++ b/screenshot-tests/src/main/AndroidManifest.xml
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest />
diff --git a/screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/core/AlertScreenshotTests.kt b/screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/core/AlertScreenshotTests.kt
new file mode 100644
index 0000000000..a2f03961ce
--- /dev/null
+++ b/screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/core/AlertScreenshotTests.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright (c) 2026 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package org.meshtastic.screenshots.core
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.tooling.preview.PreviewLightDark
+import com.android.tools.screenshot.PreviewTest
+import org.meshtastic.core.ui.util.PreviewComposableAlert
+import org.meshtastic.core.ui.util.PreviewHtmlAlert
+import org.meshtastic.core.ui.util.PreviewIconAlert
+import org.meshtastic.core.ui.util.PreviewMultipleChoiceAlert
+import org.meshtastic.core.ui.util.PreviewTextAlert
+
+@PreviewTest
+@PreviewLightDark
+@Composable
+fun ScreenshotTextAlert() {
+ PreviewTextAlert()
+}
+
+@PreviewTest
+@PreviewLightDark
+@Composable
+fun ScreenshotIconAlert() {
+ PreviewIconAlert()
+}
+
+@PreviewTest
+@PreviewLightDark
+@Composable
+fun ScreenshotHtmlAlert() {
+ PreviewHtmlAlert()
+}
+
+@PreviewTest
+@PreviewLightDark
+@Composable
+fun ScreenshotMultipleChoiceAlert() {
+ PreviewMultipleChoiceAlert()
+}
+
+@PreviewTest
+@PreviewLightDark
+@Composable
+fun ScreenshotComposableAlert() {
+ PreviewComposableAlert()
+}
diff --git a/screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/core/ComponentScreenshotTests.kt b/screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/core/ComponentScreenshotTests.kt
new file mode 100644
index 0000000000..c75102722e
--- /dev/null
+++ b/screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/core/ComponentScreenshotTests.kt
@@ -0,0 +1,137 @@
+/*
+ * Copyright (c) 2026 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package org.meshtastic.screenshots.core
+
+import androidx.compose.material3.Surface
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.tooling.preview.PreviewLightDark
+import androidx.compose.ui.tooling.preview.PreviewParameter
+import com.android.tools.screenshot.PreviewTest
+import org.meshtastic.core.model.Node
+import org.meshtastic.core.ui.component.ChannelInfoPreview
+import org.meshtastic.core.ui.component.ChannelItemPreview
+import org.meshtastic.core.ui.component.DistanceInfoPreview
+import org.meshtastic.core.ui.component.HopsInfoPreview
+import org.meshtastic.core.ui.component.LastHeardInfoPreview
+import org.meshtastic.core.ui.component.ListItemDisabledPreview
+import org.meshtastic.core.ui.component.ListItemPreview
+import org.meshtastic.core.ui.component.MaterialBatteryInfo
+import org.meshtastic.core.ui.component.MaterialBluetoothSignalInfo
+import org.meshtastic.core.ui.component.SatelliteCountInfoPreview
+import org.meshtastic.core.ui.component.SignalInfo
+import org.meshtastic.core.ui.component.SwitchListItemPreview
+import org.meshtastic.core.ui.component.TitledCardPreview
+import org.meshtastic.core.ui.component.preview.NodePreviewParameterProvider
+import org.meshtastic.core.ui.theme.AppTheme
+
+@PreviewTest
+@PreviewLightDark
+@Composable
+fun ScreenshotChannelItem() {
+ ChannelItemPreview()
+}
+
+@PreviewTest
+@PreviewLightDark
+@Composable
+fun ScreenshotListItem() {
+ ListItemPreview()
+}
+
+@PreviewTest
+@PreviewLightDark
+@Composable
+fun ScreenshotListItemDisabled() {
+ ListItemDisabledPreview()
+}
+
+@PreviewTest
+@PreviewLightDark
+@Composable
+fun ScreenshotSwitchListItem() {
+ SwitchListItemPreview()
+}
+
+@PreviewTest
+@PreviewLightDark
+@Composable
+fun ScreenshotHopsInfo() {
+ HopsInfoPreview()
+}
+
+@PreviewTest
+@PreviewLightDark
+@Composable
+fun ScreenshotSignalInfoSimple() {
+ AppTheme { SignalInfo(node = Node(num = 1, lastHeard = 0, channel = 0, snr = 12.5F, rssi = -42, hopsAway = 0)) }
+}
+
+@PreviewTest
+@PreviewLightDark
+@Composable
+fun ScreenshotSignalInfo(@PreviewParameter(NodePreviewParameterProvider::class) node: Node) {
+ AppTheme { SignalInfo(node = node) }
+}
+
+@PreviewTest
+@PreviewLightDark
+@Composable
+fun ScreenshotDistanceInfo() {
+ DistanceInfoPreview()
+}
+
+@PreviewTest
+@PreviewLightDark
+@Composable
+fun ScreenshotLastHeardInfo() {
+ LastHeardInfoPreview()
+}
+
+@PreviewTest
+@PreviewLightDark
+@Composable
+fun ScreenshotMaterialBatteryInfo() {
+ AppTheme { MaterialBatteryInfo(level = 85, voltage = 3.7F) }
+}
+
+@PreviewTest
+@PreviewLightDark
+@Composable
+fun ScreenshotTitledCard() {
+ TitledCardPreview()
+}
+
+@PreviewTest
+@PreviewLightDark
+@Composable
+fun ScreenshotChannelInfo() {
+ ChannelInfoPreview()
+}
+
+@PreviewTest
+@PreviewLightDark
+@Composable
+fun ScreenshotSatelliteCountInfo() {
+ SatelliteCountInfoPreview()
+}
+
+@PreviewTest
+@PreviewLightDark
+@Composable
+fun ScreenshotMaterialBluetoothSignalInfo() {
+ AppTheme { Surface { MaterialBluetoothSignalInfo(rssi = -65) } }
+}
diff --git a/screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTests.kt b/screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTests.kt
new file mode 100644
index 0000000000..ba9de0a6f5
--- /dev/null
+++ b/screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTests.kt
@@ -0,0 +1,181 @@
+/*
+ * Copyright (c) 2026 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package org.meshtastic.screenshots.core
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.tooling.preview.PreviewLightDark
+import com.android.tools.screenshot.PreviewTest
+import org.meshtastic.core.ui.component.BitwisePreferencePreview
+import org.meshtastic.core.ui.component.DropDownPreferencePreview
+import org.meshtastic.core.ui.component.EditIPv4PreferencePreview
+import org.meshtastic.core.ui.component.EditListPreferencePreview
+import org.meshtastic.core.ui.component.EditPasswordPreferencePreview
+import org.meshtastic.core.ui.component.EditTextPreferencePreview
+import org.meshtastic.core.ui.component.ElevationInfoPreview
+import org.meshtastic.core.ui.component.IAQScalePreview
+import org.meshtastic.core.ui.component.IconInfoPreview
+import org.meshtastic.core.ui.component.NodeChipPreview
+import org.meshtastic.core.ui.component.PositionPrecisionPreferencePreview
+import org.meshtastic.core.ui.component.PreferenceCategoryPreview
+import org.meshtastic.core.ui.component.PreviewAllSecurityIconsWithDialog
+import org.meshtastic.core.ui.component.PreviewImportFABChannel
+import org.meshtastic.core.ui.component.PreviewImportFABContact
+import org.meshtastic.core.ui.component.RegularPreferencePreview
+import org.meshtastic.core.ui.component.SliderPreferenceDisabledPreview
+import org.meshtastic.core.ui.component.SliderPreferencePreview
+import org.meshtastic.core.ui.component.SwitchPreferencePreview
+import org.meshtastic.core.ui.component.TextDividerPreferencePreview
+
+@PreviewTest
+@PreviewLightDark
+@Composable
+fun ScreenshotSliderPreference() {
+ SliderPreferencePreview()
+}
+
+@PreviewTest
+@PreviewLightDark
+@Composable
+fun ScreenshotSliderPreferenceDisabled() {
+ SliderPreferenceDisabledPreview()
+}
+
+@PreviewTest
+@PreviewLightDark
+@Composable
+fun ScreenshotEditTextPreference() {
+ EditTextPreferencePreview()
+}
+
+@PreviewTest
+@PreviewLightDark
+@Composable
+fun ScreenshotEditListPreference() {
+ EditListPreferencePreview()
+}
+
+@PreviewTest
+@PreviewLightDark
+@Composable
+fun ScreenshotSwitchPreference() {
+ SwitchPreferencePreview()
+}
+
+@PreviewTest
+@PreviewLightDark
+@Composable
+fun ScreenshotBitwisePreference() {
+ BitwisePreferencePreview()
+}
+
+@PreviewTest
+@PreviewLightDark
+@Composable
+fun ScreenshotPositionPrecisionPreference() {
+ PositionPrecisionPreferencePreview()
+}
+
+@PreviewTest
+@PreviewLightDark
+@Composable
+fun ScreenshotRegularPreference() {
+ RegularPreferencePreview()
+}
+
+@PreviewTest
+@PreviewLightDark
+@Composable
+fun ScreenshotDropDownPreference() {
+ DropDownPreferencePreview()
+}
+
+@PreviewTest
+@PreviewLightDark
+@Composable
+fun ScreenshotEditIPv4Preference() {
+ EditIPv4PreferencePreview()
+}
+
+@PreviewTest
+@PreviewLightDark
+@Composable
+fun ScreenshotEditPasswordPreference() {
+ EditPasswordPreferencePreview()
+}
+
+@PreviewTest
+@PreviewLightDark
+@Composable
+fun ScreenshotPreferenceCategory() {
+ PreferenceCategoryPreview()
+}
+
+@PreviewTest
+@PreviewLightDark
+@Composable
+fun ScreenshotTextDividerPreference() {
+ TextDividerPreferencePreview()
+}
+
+@PreviewTest
+@PreviewLightDark
+@Composable
+fun ScreenshotElevationInfo() {
+ ElevationInfoPreview()
+}
+
+@PreviewTest
+@PreviewLightDark
+@Composable
+fun ScreenshotAllSecurityIcons() {
+ PreviewAllSecurityIconsWithDialog()
+}
+
+@PreviewTest
+@PreviewLightDark
+@Composable
+fun ScreenshotIAQScale() {
+ IAQScalePreview()
+}
+
+@PreviewTest
+@PreviewLightDark
+@Composable
+fun ScreenshotNodeChip() {
+ NodeChipPreview()
+}
+
+@PreviewTest
+@PreviewLightDark
+@Composable
+fun ScreenshotIconInfo() {
+ IconInfoPreview()
+}
+
+@PreviewTest
+@PreviewLightDark
+@Composable
+fun ScreenshotImportFABContact() {
+ PreviewImportFABContact()
+}
+
+@PreviewTest
+@PreviewLightDark
+@Composable
+fun ScreenshotImportFABChannel() {
+ PreviewImportFABChannel()
+}
diff --git a/screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/feature/ConnectionsScreenshotTests.kt b/screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/feature/ConnectionsScreenshotTests.kt
new file mode 100644
index 0000000000..367d38a16f
--- /dev/null
+++ b/screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/feature/ConnectionsScreenshotTests.kt
@@ -0,0 +1,69 @@
+/*
+ * Copyright (c) 2026 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package org.meshtastic.screenshots.feature
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.tooling.preview.PreviewLightDark
+import com.android.tools.screenshot.PreviewTest
+import org.meshtastic.feature.connections.component.ConnectingDeviceInfoPreview
+import org.meshtastic.feature.connections.component.DeviceListItemPreview
+import org.meshtastic.feature.connections.component.DeviceSectionHeaderPreview
+import org.meshtastic.feature.connections.component.DisconnectButtonPreview
+import org.meshtastic.feature.connections.component.EmptyStateContentPreview
+import org.meshtastic.feature.connections.component.TransportFilterChipsPreview
+
+@PreviewTest
+@PreviewLightDark
+@Composable
+fun ScreenshotDeviceListItem() {
+ DeviceListItemPreview()
+}
+
+@PreviewTest
+@PreviewLightDark
+@Composable
+fun ScreenshotDisconnectButton() {
+ DisconnectButtonPreview()
+}
+
+@PreviewTest
+@PreviewLightDark
+@Composable
+fun ScreenshotConnectingDeviceInfo() {
+ ConnectingDeviceInfoPreview()
+}
+
+@PreviewTest
+@PreviewLightDark
+@Composable
+fun ScreenshotEmptyStateContent() {
+ EmptyStateContentPreview()
+}
+
+@PreviewTest
+@PreviewLightDark
+@Composable
+fun ScreenshotDeviceSectionHeader() {
+ DeviceSectionHeaderPreview()
+}
+
+@PreviewTest
+@PreviewLightDark
+@Composable
+fun ScreenshotTransportFilterChips() {
+ TransportFilterChipsPreview()
+}
diff --git a/screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/feature/FirmwareScreenshotTests.kt b/screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/feature/FirmwareScreenshotTests.kt
new file mode 100644
index 0000000000..831b3afd53
--- /dev/null
+++ b/screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/feature/FirmwareScreenshotTests.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright (c) 2026 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package org.meshtastic.screenshots.feature
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.tooling.preview.PreviewLightDark
+import com.android.tools.screenshot.PreviewTest
+import org.meshtastic.feature.firmware.CheckingStatePreview
+import org.meshtastic.feature.firmware.DisclaimerDialogPreview
+import org.meshtastic.feature.firmware.ErrorStatePreview
+import org.meshtastic.feature.firmware.SuccessStatePreview
+import org.meshtastic.feature.firmware.VerifyingStatePreview
+
+@PreviewTest
+@PreviewLightDark
+@Composable
+fun ScreenshotFirmwareVerifying() {
+ VerifyingStatePreview()
+}
+
+@PreviewTest
+@PreviewLightDark
+@Composable
+fun ScreenshotFirmwareChecking() {
+ CheckingStatePreview()
+}
+
+@PreviewTest
+@PreviewLightDark
+@Composable
+fun ScreenshotFirmwareError() {
+ ErrorStatePreview()
+}
+
+@PreviewTest
+@PreviewLightDark
+@Composable
+fun ScreenshotFirmwareSuccess() {
+ SuccessStatePreview()
+}
+
+@PreviewTest
+@PreviewLightDark
+@Composable
+fun ScreenshotFirmwareDisclaimer() {
+ DisclaimerDialogPreview()
+}
diff --git a/screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/feature/IntroScreenshotTests.kt b/screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/feature/IntroScreenshotTests.kt
new file mode 100644
index 0000000000..823ae3a90d
--- /dev/null
+++ b/screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/feature/IntroScreenshotTests.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (c) 2026 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package org.meshtastic.screenshots.feature
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.tooling.preview.PreviewLightDark
+import com.android.tools.screenshot.PreviewTest
+import org.meshtastic.feature.intro.WelcomeScreenPreview
+
+@PreviewTest
+@PreviewLightDark
+@Composable
+fun ScreenshotWelcomeScreen() {
+ WelcomeScreenPreview()
+}
diff --git a/screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/feature/MessagingScreenshotTests.kt b/screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/feature/MessagingScreenshotTests.kt
new file mode 100644
index 0000000000..278575912a
--- /dev/null
+++ b/screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/feature/MessagingScreenshotTests.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright (c) 2026 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package org.meshtastic.screenshots.feature
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.tooling.preview.PreviewLightDark
+import com.android.tools.screenshot.PreviewTest
+import org.meshtastic.feature.messaging.EditQuickChatDialogPreview
+import org.meshtastic.feature.messaging.MessageInputPreview
+import org.meshtastic.feature.messaging.QuickChatItemPreview
+import org.meshtastic.feature.messaging.component.ReactionItemPreview
+
+@PreviewTest
+@PreviewLightDark
+@Composable
+fun ScreenshotQuickChatItem() {
+ QuickChatItemPreview()
+}
+
+@PreviewTest
+@PreviewLightDark
+@Composable
+fun ScreenshotEditQuickChatDialog() {
+ EditQuickChatDialogPreview()
+}
+
+@PreviewTest
+@PreviewLightDark
+@Composable
+fun ScreenshotMessageInput() {
+ MessageInputPreview()
+}
+
+@PreviewTest
+@PreviewLightDark
+@Composable
+fun ScreenshotReactionItem() {
+ ReactionItemPreview()
+}
diff --git a/screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/feature/NodeScreenshotTests.kt b/screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/feature/NodeScreenshotTests.kt
new file mode 100644
index 0000000000..0074f91925
--- /dev/null
+++ b/screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/feature/NodeScreenshotTests.kt
@@ -0,0 +1,117 @@
+/*
+ * Copyright (c) 2026 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package org.meshtastic.screenshots.feature
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.tooling.preview.PreviewLightDark
+import com.android.tools.screenshot.PreviewTest
+import org.meshtastic.feature.node.component.DeviceActionsLocalPreview
+import org.meshtastic.feature.node.component.DeviceActionsRemotePreview
+import org.meshtastic.feature.node.component.NodeDetailsSectionPreview
+import org.meshtastic.feature.node.component.PositionInlineContentPreview
+import org.meshtastic.feature.node.component.TelemetricActionsSectionEmptyPreview
+import org.meshtastic.feature.node.component.TelemetricActionsSectionPreview
+import org.meshtastic.feature.node.detail.NodeDetailContentLoadingPreview
+import org.meshtastic.feature.node.detail.NodeDetailContentLocalPreview
+import org.meshtastic.feature.node.detail.NodeDetailContentRemotePreview
+import org.meshtastic.feature.node.metrics.DeviceMetricsCardPreview
+import org.meshtastic.feature.node.metrics.LegendPreview
+import org.meshtastic.feature.node.metrics.PreviewEnvironmentMetricsContent
+
+@PreviewTest
+@PreviewLightDark
+@Composable
+fun ScreenshotDeviceActionsRemote() {
+ DeviceActionsRemotePreview()
+}
+
+@PreviewTest
+@PreviewLightDark
+@Composable
+fun ScreenshotDeviceActionsLocal() {
+ DeviceActionsLocalPreview()
+}
+
+@PreviewTest
+@PreviewLightDark
+@Composable
+fun ScreenshotTelemetricActionsSection() {
+ TelemetricActionsSectionPreview()
+}
+
+@PreviewTest
+@PreviewLightDark
+@Composable
+fun ScreenshotTelemetricActionsSectionEmpty() {
+ TelemetricActionsSectionEmptyPreview()
+}
+
+@PreviewTest
+@PreviewLightDark
+@Composable
+fun ScreenshotPositionInlineContent() {
+ PositionInlineContentPreview()
+}
+
+@PreviewTest
+@PreviewLightDark
+@Composable
+fun ScreenshotNodeDetailsSection() {
+ NodeDetailsSectionPreview()
+}
+
+@PreviewTest
+@PreviewLightDark
+@Composable
+fun ScreenshotNodeDetailContentRemote() {
+ NodeDetailContentRemotePreview()
+}
+
+@PreviewTest
+@PreviewLightDark
+@Composable
+fun ScreenshotNodeDetailContentLocal() {
+ NodeDetailContentLocalPreview()
+}
+
+@PreviewTest
+@PreviewLightDark
+@Composable
+fun ScreenshotNodeDetailContentLoading() {
+ NodeDetailContentLoadingPreview()
+}
+
+@PreviewTest
+@PreviewLightDark
+@Composable
+fun ScreenshotLegend() {
+ LegendPreview()
+}
+
+@PreviewTest
+@PreviewLightDark
+@Composable
+fun ScreenshotDeviceMetricsCard() {
+ DeviceMetricsCardPreview()
+}
+
+@PreviewTest
+@PreviewLightDark
+@Composable
+fun ScreenshotEnvironmentMetricsContent() {
+ PreviewEnvironmentMetricsContent()
+}
diff --git a/screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/feature/SettingsScreenshotTests.kt b/screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/feature/SettingsScreenshotTests.kt
new file mode 100644
index 0000000000..6475095785
--- /dev/null
+++ b/screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/feature/SettingsScreenshotTests.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright (c) 2026 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package org.meshtastic.screenshots.feature
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.tooling.preview.PreviewLightDark
+import com.android.tools.screenshot.PreviewTest
+import org.meshtastic.feature.settings.component.AppInfoSectionPreview
+import org.meshtastic.feature.settings.component.AppearanceSectionPreview
+import org.meshtastic.feature.settings.component.PersistenceSectionPreview
+
+@PreviewTest
+@PreviewLightDark
+@Composable
+fun ScreenshotAppearanceSection() {
+ AppearanceSectionPreview()
+}
+
+@PreviewTest
+@PreviewLightDark
+@Composable
+fun ScreenshotPersistenceSection() {
+ PersistenceSectionPreview()
+}
+
+@PreviewTest
+@PreviewLightDark
+@Composable
+fun ScreenshotAppInfoSection() {
+ AppInfoSectionPreview()
+}
diff --git a/screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTests.kt b/screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTests.kt
new file mode 100644
index 0000000000..e9354f617b
--- /dev/null
+++ b/screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTests.kt
@@ -0,0 +1,85 @@
+/*
+ * Copyright (c) 2026 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package org.meshtastic.screenshots.feature
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.tooling.preview.PreviewLightDark
+import com.android.tools.screenshot.PreviewTest
+import org.meshtastic.feature.wifiprovision.ui.ConnectedFailedPreview
+import org.meshtastic.feature.wifiprovision.ui.ConnectedSuccessPreview
+import org.meshtastic.feature.wifiprovision.ui.ConnectedWithNetworksPreview
+import org.meshtastic.feature.wifiprovision.ui.DeviceFoundPreview
+import org.meshtastic.feature.wifiprovision.ui.MpwrdDisclaimerBannerPreview
+import org.meshtastic.feature.wifiprovision.ui.NetworkRowPreview
+import org.meshtastic.feature.wifiprovision.ui.ProvisionStatusCardSuccessPreview
+import org.meshtastic.feature.wifiprovision.ui.ScanningBlePreview
+
+@PreviewTest
+@PreviewLightDark
+@Composable
+fun ScreenshotScanningBle() {
+ ScanningBlePreview()
+}
+
+@PreviewTest
+@PreviewLightDark
+@Composable
+fun ScreenshotDeviceFound() {
+ DeviceFoundPreview()
+}
+
+@PreviewTest
+@PreviewLightDark
+@Composable
+fun ScreenshotConnectedWithNetworks() {
+ ConnectedWithNetworksPreview()
+}
+
+@PreviewTest
+@PreviewLightDark
+@Composable
+fun ScreenshotConnectedSuccess() {
+ ConnectedSuccessPreview()
+}
+
+@PreviewTest
+@PreviewLightDark
+@Composable
+fun ScreenshotConnectedFailed() {
+ ConnectedFailedPreview()
+}
+
+@PreviewTest
+@PreviewLightDark
+@Composable
+fun ScreenshotNetworkRow() {
+ NetworkRowPreview()
+}
+
+@PreviewTest
+@PreviewLightDark
+@Composable
+fun ScreenshotProvisionStatusCardSuccess() {
+ ProvisionStatusCardSuccessPreview()
+}
+
+@PreviewTest
+@PreviewLightDark
+@Composable
+fun ScreenshotMpwrdDisclaimerBanner() {
+ MpwrdDisclaimerBannerPreview()
+}
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/AlertScreenshotTestsKt/ScreenshotComposableAlert_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/AlertScreenshotTestsKt/ScreenshotComposableAlert_Dark_d19fbf1f_0.png
new file mode 100644
index 0000000000..8a2f075a41
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/AlertScreenshotTestsKt/ScreenshotComposableAlert_Dark_d19fbf1f_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/AlertScreenshotTestsKt/ScreenshotComposableAlert_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/AlertScreenshotTestsKt/ScreenshotComposableAlert_Light_b29dc7a7_0.png
new file mode 100644
index 0000000000..dfbcea3181
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/AlertScreenshotTestsKt/ScreenshotComposableAlert_Light_b29dc7a7_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/AlertScreenshotTestsKt/ScreenshotHtmlAlert_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/AlertScreenshotTestsKt/ScreenshotHtmlAlert_Dark_d19fbf1f_0.png
new file mode 100644
index 0000000000..ab77a693dd
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/AlertScreenshotTestsKt/ScreenshotHtmlAlert_Dark_d19fbf1f_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/AlertScreenshotTestsKt/ScreenshotHtmlAlert_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/AlertScreenshotTestsKt/ScreenshotHtmlAlert_Light_b29dc7a7_0.png
new file mode 100644
index 0000000000..e10f03b4b1
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/AlertScreenshotTestsKt/ScreenshotHtmlAlert_Light_b29dc7a7_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/AlertScreenshotTestsKt/ScreenshotIconAlert_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/AlertScreenshotTestsKt/ScreenshotIconAlert_Dark_d19fbf1f_0.png
new file mode 100644
index 0000000000..5c2efc58d6
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/AlertScreenshotTestsKt/ScreenshotIconAlert_Dark_d19fbf1f_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/AlertScreenshotTestsKt/ScreenshotIconAlert_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/AlertScreenshotTestsKt/ScreenshotIconAlert_Light_b29dc7a7_0.png
new file mode 100644
index 0000000000..a0e4aa13b8
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/AlertScreenshotTestsKt/ScreenshotIconAlert_Light_b29dc7a7_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/AlertScreenshotTestsKt/ScreenshotMultipleChoiceAlert_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/AlertScreenshotTestsKt/ScreenshotMultipleChoiceAlert_Dark_d19fbf1f_0.png
new file mode 100644
index 0000000000..6a66b82108
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/AlertScreenshotTestsKt/ScreenshotMultipleChoiceAlert_Dark_d19fbf1f_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/AlertScreenshotTestsKt/ScreenshotMultipleChoiceAlert_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/AlertScreenshotTestsKt/ScreenshotMultipleChoiceAlert_Light_b29dc7a7_0.png
new file mode 100644
index 0000000000..7e54d473dd
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/AlertScreenshotTestsKt/ScreenshotMultipleChoiceAlert_Light_b29dc7a7_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/AlertScreenshotTestsKt/ScreenshotTextAlert_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/AlertScreenshotTestsKt/ScreenshotTextAlert_Dark_d19fbf1f_0.png
new file mode 100644
index 0000000000..a317c423f0
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/AlertScreenshotTestsKt/ScreenshotTextAlert_Dark_d19fbf1f_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/AlertScreenshotTestsKt/ScreenshotTextAlert_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/AlertScreenshotTestsKt/ScreenshotTextAlert_Light_b29dc7a7_0.png
new file mode 100644
index 0000000000..0fd4fa0505
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/AlertScreenshotTestsKt/ScreenshotTextAlert_Light_b29dc7a7_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotChannelInfo_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotChannelInfo_Dark_d19fbf1f_0.png
new file mode 100644
index 0000000000..2f7afde447
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotChannelInfo_Dark_d19fbf1f_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotChannelInfo_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotChannelInfo_Light_b29dc7a7_0.png
new file mode 100644
index 0000000000..6f022378f8
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotChannelInfo_Light_b29dc7a7_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotChannelItem_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotChannelItem_Dark_d19fbf1f_0.png
new file mode 100644
index 0000000000..1bca5bdc47
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotChannelItem_Dark_d19fbf1f_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotChannelItem_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotChannelItem_Light_b29dc7a7_0.png
new file mode 100644
index 0000000000..06d194c769
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotChannelItem_Light_b29dc7a7_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotDistanceInfo_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotDistanceInfo_Dark_d19fbf1f_0.png
new file mode 100644
index 0000000000..eb707dac90
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotDistanceInfo_Dark_d19fbf1f_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotDistanceInfo_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotDistanceInfo_Light_b29dc7a7_0.png
new file mode 100644
index 0000000000..a9fac3e1d9
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotDistanceInfo_Light_b29dc7a7_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotHopsInfo_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotHopsInfo_Dark_d19fbf1f_0.png
new file mode 100644
index 0000000000..73001ba495
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotHopsInfo_Dark_d19fbf1f_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotHopsInfo_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotHopsInfo_Light_b29dc7a7_0.png
new file mode 100644
index 0000000000..82cdee0394
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotHopsInfo_Light_b29dc7a7_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotLastHeardInfo_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotLastHeardInfo_Dark_d19fbf1f_0.png
new file mode 100644
index 0000000000..075a1d49c7
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotLastHeardInfo_Dark_d19fbf1f_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotLastHeardInfo_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotLastHeardInfo_Light_b29dc7a7_0.png
new file mode 100644
index 0000000000..824627b2af
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotLastHeardInfo_Light_b29dc7a7_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotListItemDisabled_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotListItemDisabled_Dark_d19fbf1f_0.png
new file mode 100644
index 0000000000..1d279597fe
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotListItemDisabled_Dark_d19fbf1f_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotListItemDisabled_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotListItemDisabled_Light_b29dc7a7_0.png
new file mode 100644
index 0000000000..1d279597fe
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotListItemDisabled_Light_b29dc7a7_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotListItem_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotListItem_Dark_d19fbf1f_0.png
new file mode 100644
index 0000000000..1d279597fe
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotListItem_Dark_d19fbf1f_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotListItem_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotListItem_Light_b29dc7a7_0.png
new file mode 100644
index 0000000000..1d279597fe
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotListItem_Light_b29dc7a7_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotMaterialBatteryInfo_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotMaterialBatteryInfo_Dark_d19fbf1f_0.png
new file mode 100644
index 0000000000..95505f4e3e
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotMaterialBatteryInfo_Dark_d19fbf1f_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotMaterialBatteryInfo_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotMaterialBatteryInfo_Light_b29dc7a7_0.png
new file mode 100644
index 0000000000..b936f8b183
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotMaterialBatteryInfo_Light_b29dc7a7_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotMaterialBluetoothSignalInfo_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotMaterialBluetoothSignalInfo_Dark_d19fbf1f_0.png
new file mode 100644
index 0000000000..eb78c3552a
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotMaterialBluetoothSignalInfo_Dark_d19fbf1f_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotMaterialBluetoothSignalInfo_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotMaterialBluetoothSignalInfo_Light_b29dc7a7_0.png
new file mode 100644
index 0000000000..327b54ac7f
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotMaterialBluetoothSignalInfo_Light_b29dc7a7_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotSatelliteCountInfo_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotSatelliteCountInfo_Dark_d19fbf1f_0.png
new file mode 100644
index 0000000000..2d5e13eac9
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotSatelliteCountInfo_Dark_d19fbf1f_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotSatelliteCountInfo_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotSatelliteCountInfo_Light_b29dc7a7_0.png
new file mode 100644
index 0000000000..9716a4bbbf
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotSatelliteCountInfo_Light_b29dc7a7_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotSignalInfoSimple_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotSignalInfoSimple_Dark_d19fbf1f_0.png
new file mode 100644
index 0000000000..3a9addf1a7
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotSignalInfoSimple_Dark_d19fbf1f_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotSignalInfoSimple_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotSignalInfoSimple_Light_b29dc7a7_0.png
new file mode 100644
index 0000000000..3a9addf1a7
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotSignalInfoSimple_Light_b29dc7a7_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotSignalInfo_Dark_d19fbf1f_41783942_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotSignalInfo_Dark_d19fbf1f_41783942_0.png
new file mode 100644
index 0000000000..3a9addf1a7
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotSignalInfo_Dark_d19fbf1f_41783942_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotSignalInfo_Dark_d19fbf1f_41783942_1.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotSignalInfo_Dark_d19fbf1f_41783942_1.png
new file mode 100644
index 0000000000..3a9addf1a7
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotSignalInfo_Dark_d19fbf1f_41783942_1.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotSignalInfo_Dark_d19fbf1f_41783942_2.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotSignalInfo_Dark_d19fbf1f_41783942_2.png
new file mode 100644
index 0000000000..bd01926438
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotSignalInfo_Dark_d19fbf1f_41783942_2.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotSignalInfo_Dark_d19fbf1f_41783942_3.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotSignalInfo_Dark_d19fbf1f_41783942_3.png
new file mode 100644
index 0000000000..3a9addf1a7
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotSignalInfo_Dark_d19fbf1f_41783942_3.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotSignalInfo_Dark_d19fbf1f_41783942_4.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotSignalInfo_Dark_d19fbf1f_41783942_4.png
new file mode 100644
index 0000000000..3a9addf1a7
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotSignalInfo_Dark_d19fbf1f_41783942_4.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotSignalInfo_Light_b29dc7a7_41783942_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotSignalInfo_Light_b29dc7a7_41783942_0.png
new file mode 100644
index 0000000000..3a9addf1a7
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotSignalInfo_Light_b29dc7a7_41783942_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotSignalInfo_Light_b29dc7a7_41783942_1.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotSignalInfo_Light_b29dc7a7_41783942_1.png
new file mode 100644
index 0000000000..3a9addf1a7
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotSignalInfo_Light_b29dc7a7_41783942_1.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotSignalInfo_Light_b29dc7a7_41783942_2.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotSignalInfo_Light_b29dc7a7_41783942_2.png
new file mode 100644
index 0000000000..bd01926438
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotSignalInfo_Light_b29dc7a7_41783942_2.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotSignalInfo_Light_b29dc7a7_41783942_3.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotSignalInfo_Light_b29dc7a7_41783942_3.png
new file mode 100644
index 0000000000..3a9addf1a7
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotSignalInfo_Light_b29dc7a7_41783942_3.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotSignalInfo_Light_b29dc7a7_41783942_4.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotSignalInfo_Light_b29dc7a7_41783942_4.png
new file mode 100644
index 0000000000..3a9addf1a7
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotSignalInfo_Light_b29dc7a7_41783942_4.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotSwitchListItem_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotSwitchListItem_Dark_d19fbf1f_0.png
new file mode 100644
index 0000000000..77eb25dc52
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotSwitchListItem_Dark_d19fbf1f_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotSwitchListItem_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotSwitchListItem_Light_b29dc7a7_0.png
new file mode 100644
index 0000000000..6498fa6b49
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotSwitchListItem_Light_b29dc7a7_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotTitledCard_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotTitledCard_Dark_d19fbf1f_0.png
new file mode 100644
index 0000000000..6bdbe024ee
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotTitledCard_Dark_d19fbf1f_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotTitledCard_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotTitledCard_Light_b29dc7a7_0.png
new file mode 100644
index 0000000000..a97e333dac
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/ComponentScreenshotTestsKt/ScreenshotTitledCard_Light_b29dc7a7_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotAllSecurityIcons_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotAllSecurityIcons_Dark_d19fbf1f_0.png
new file mode 100644
index 0000000000..85a2041413
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotAllSecurityIcons_Dark_d19fbf1f_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotAllSecurityIcons_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotAllSecurityIcons_Light_b29dc7a7_0.png
new file mode 100644
index 0000000000..85a2041413
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotAllSecurityIcons_Light_b29dc7a7_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotBitwisePreference_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotBitwisePreference_Dark_d19fbf1f_0.png
new file mode 100644
index 0000000000..db22dd681d
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotBitwisePreference_Dark_d19fbf1f_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotBitwisePreference_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotBitwisePreference_Light_b29dc7a7_0.png
new file mode 100644
index 0000000000..f778a35f6e
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotBitwisePreference_Light_b29dc7a7_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotDropDownPreference_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotDropDownPreference_Dark_d19fbf1f_0.png
new file mode 100644
index 0000000000..5a3ffeb885
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotDropDownPreference_Dark_d19fbf1f_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotDropDownPreference_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotDropDownPreference_Light_b29dc7a7_0.png
new file mode 100644
index 0000000000..53419996a6
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotDropDownPreference_Light_b29dc7a7_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotEditIPv4Preference_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotEditIPv4Preference_Dark_d19fbf1f_0.png
new file mode 100644
index 0000000000..4a10d4a873
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotEditIPv4Preference_Dark_d19fbf1f_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotEditIPv4Preference_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotEditIPv4Preference_Light_b29dc7a7_0.png
new file mode 100644
index 0000000000..f99a069871
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotEditIPv4Preference_Light_b29dc7a7_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotEditListPreference_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotEditListPreference_Dark_d19fbf1f_0.png
new file mode 100644
index 0000000000..db80357b6c
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotEditListPreference_Dark_d19fbf1f_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotEditListPreference_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotEditListPreference_Light_b29dc7a7_0.png
new file mode 100644
index 0000000000..b7b0364a97
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotEditListPreference_Light_b29dc7a7_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotEditPasswordPreference_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotEditPasswordPreference_Dark_d19fbf1f_0.png
new file mode 100644
index 0000000000..664856a8b4
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotEditPasswordPreference_Dark_d19fbf1f_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotEditPasswordPreference_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotEditPasswordPreference_Light_b29dc7a7_0.png
new file mode 100644
index 0000000000..90400f3c61
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotEditPasswordPreference_Light_b29dc7a7_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotEditTextPreference_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotEditTextPreference_Dark_d19fbf1f_0.png
new file mode 100644
index 0000000000..3ea524b354
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotEditTextPreference_Dark_d19fbf1f_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotEditTextPreference_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotEditTextPreference_Light_b29dc7a7_0.png
new file mode 100644
index 0000000000..679e0da204
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotEditTextPreference_Light_b29dc7a7_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotElevationInfo_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotElevationInfo_Dark_d19fbf1f_0.png
new file mode 100644
index 0000000000..7475764b97
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotElevationInfo_Dark_d19fbf1f_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotElevationInfo_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotElevationInfo_Light_b29dc7a7_0.png
new file mode 100644
index 0000000000..a23cc014c5
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotElevationInfo_Light_b29dc7a7_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotIAQScale_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotIAQScale_Dark_d19fbf1f_0.png
new file mode 100644
index 0000000000..23a4a65589
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotIAQScale_Dark_d19fbf1f_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotIAQScale_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotIAQScale_Light_b29dc7a7_0.png
new file mode 100644
index 0000000000..23a4a65589
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotIAQScale_Light_b29dc7a7_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotIconInfo_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotIconInfo_Dark_d19fbf1f_0.png
new file mode 100644
index 0000000000..0b5c90553e
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotIconInfo_Dark_d19fbf1f_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotIconInfo_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotIconInfo_Light_b29dc7a7_0.png
new file mode 100644
index 0000000000..fcb6dc18dd
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotIconInfo_Light_b29dc7a7_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotImportFABChannel_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotImportFABChannel_Dark_d19fbf1f_0.png
new file mode 100644
index 0000000000..6dc55a5206
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotImportFABChannel_Dark_d19fbf1f_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotImportFABChannel_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotImportFABChannel_Light_b29dc7a7_0.png
new file mode 100644
index 0000000000..2718ac63e6
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotImportFABChannel_Light_b29dc7a7_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotImportFABContact_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotImportFABContact_Dark_d19fbf1f_0.png
new file mode 100644
index 0000000000..6dc55a5206
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotImportFABContact_Dark_d19fbf1f_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotImportFABContact_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotImportFABContact_Light_b29dc7a7_0.png
new file mode 100644
index 0000000000..2718ac63e6
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotImportFABContact_Light_b29dc7a7_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotNodeChip_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotNodeChip_Dark_d19fbf1f_0.png
new file mode 100644
index 0000000000..5a06d5a738
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotNodeChip_Dark_d19fbf1f_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotNodeChip_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotNodeChip_Light_b29dc7a7_0.png
new file mode 100644
index 0000000000..5a06d5a738
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotNodeChip_Light_b29dc7a7_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotPositionPrecisionPreference_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotPositionPrecisionPreference_Dark_d19fbf1f_0.png
new file mode 100644
index 0000000000..7074db2027
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotPositionPrecisionPreference_Dark_d19fbf1f_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotPositionPrecisionPreference_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotPositionPrecisionPreference_Light_b29dc7a7_0.png
new file mode 100644
index 0000000000..4139cde48d
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotPositionPrecisionPreference_Light_b29dc7a7_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotPreferenceCategory_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotPreferenceCategory_Dark_d19fbf1f_0.png
new file mode 100644
index 0000000000..29312609d4
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotPreferenceCategory_Dark_d19fbf1f_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotPreferenceCategory_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotPreferenceCategory_Light_b29dc7a7_0.png
new file mode 100644
index 0000000000..29312609d4
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotPreferenceCategory_Light_b29dc7a7_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotRegularPreference_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotRegularPreference_Dark_d19fbf1f_0.png
new file mode 100644
index 0000000000..d392e5abfb
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotRegularPreference_Dark_d19fbf1f_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotRegularPreference_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotRegularPreference_Light_b29dc7a7_0.png
new file mode 100644
index 0000000000..69b55f4832
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotRegularPreference_Light_b29dc7a7_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotSliderPreferenceDisabled_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotSliderPreferenceDisabled_Dark_d19fbf1f_0.png
new file mode 100644
index 0000000000..3a9df05fc1
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotSliderPreferenceDisabled_Dark_d19fbf1f_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotSliderPreferenceDisabled_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotSliderPreferenceDisabled_Light_b29dc7a7_0.png
new file mode 100644
index 0000000000..d10163aab8
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotSliderPreferenceDisabled_Light_b29dc7a7_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotSliderPreference_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotSliderPreference_Dark_d19fbf1f_0.png
new file mode 100644
index 0000000000..1130fdcfd7
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotSliderPreference_Dark_d19fbf1f_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotSliderPreference_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotSliderPreference_Light_b29dc7a7_0.png
new file mode 100644
index 0000000000..f05643897b
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotSliderPreference_Light_b29dc7a7_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotSwitchPreference_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotSwitchPreference_Dark_d19fbf1f_0.png
new file mode 100644
index 0000000000..0f2ab59761
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotSwitchPreference_Dark_d19fbf1f_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotSwitchPreference_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotSwitchPreference_Light_b29dc7a7_0.png
new file mode 100644
index 0000000000..c1b4557551
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotSwitchPreference_Light_b29dc7a7_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotTextDividerPreference_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotTextDividerPreference_Dark_d19fbf1f_0.png
new file mode 100644
index 0000000000..4c45f42be8
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotTextDividerPreference_Dark_d19fbf1f_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotTextDividerPreference_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotTextDividerPreference_Light_b29dc7a7_0.png
new file mode 100644
index 0000000000..01537fb615
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTestsKt/ScreenshotTextDividerPreference_Light_b29dc7a7_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/ConnectionsScreenshotTestsKt/ScreenshotConnectingDeviceInfo_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/ConnectionsScreenshotTestsKt/ScreenshotConnectingDeviceInfo_Dark_d19fbf1f_0.png
new file mode 100644
index 0000000000..6425535d29
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/ConnectionsScreenshotTestsKt/ScreenshotConnectingDeviceInfo_Dark_d19fbf1f_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/ConnectionsScreenshotTestsKt/ScreenshotConnectingDeviceInfo_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/ConnectionsScreenshotTestsKt/ScreenshotConnectingDeviceInfo_Light_b29dc7a7_0.png
new file mode 100644
index 0000000000..1fc10a657f
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/ConnectionsScreenshotTestsKt/ScreenshotConnectingDeviceInfo_Light_b29dc7a7_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/ConnectionsScreenshotTestsKt/ScreenshotDeviceListItem_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/ConnectionsScreenshotTestsKt/ScreenshotDeviceListItem_Dark_d19fbf1f_0.png
new file mode 100644
index 0000000000..d212cbd299
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/ConnectionsScreenshotTestsKt/ScreenshotDeviceListItem_Dark_d19fbf1f_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/ConnectionsScreenshotTestsKt/ScreenshotDeviceListItem_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/ConnectionsScreenshotTestsKt/ScreenshotDeviceListItem_Light_b29dc7a7_0.png
new file mode 100644
index 0000000000..6377595580
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/ConnectionsScreenshotTestsKt/ScreenshotDeviceListItem_Light_b29dc7a7_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/ConnectionsScreenshotTestsKt/ScreenshotDeviceSectionHeader_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/ConnectionsScreenshotTestsKt/ScreenshotDeviceSectionHeader_Dark_d19fbf1f_0.png
new file mode 100644
index 0000000000..313bda88bb
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/ConnectionsScreenshotTestsKt/ScreenshotDeviceSectionHeader_Dark_d19fbf1f_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/ConnectionsScreenshotTestsKt/ScreenshotDeviceSectionHeader_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/ConnectionsScreenshotTestsKt/ScreenshotDeviceSectionHeader_Light_b29dc7a7_0.png
new file mode 100644
index 0000000000..b26931092b
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/ConnectionsScreenshotTestsKt/ScreenshotDeviceSectionHeader_Light_b29dc7a7_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/ConnectionsScreenshotTestsKt/ScreenshotDisconnectButton_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/ConnectionsScreenshotTestsKt/ScreenshotDisconnectButton_Dark_d19fbf1f_0.png
new file mode 100644
index 0000000000..a8541deb7f
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/ConnectionsScreenshotTestsKt/ScreenshotDisconnectButton_Dark_d19fbf1f_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/ConnectionsScreenshotTestsKt/ScreenshotDisconnectButton_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/ConnectionsScreenshotTestsKt/ScreenshotDisconnectButton_Light_b29dc7a7_0.png
new file mode 100644
index 0000000000..1ba2c24381
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/ConnectionsScreenshotTestsKt/ScreenshotDisconnectButton_Light_b29dc7a7_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/ConnectionsScreenshotTestsKt/ScreenshotEmptyStateContent_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/ConnectionsScreenshotTestsKt/ScreenshotEmptyStateContent_Dark_d19fbf1f_0.png
new file mode 100644
index 0000000000..86ab86bf35
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/ConnectionsScreenshotTestsKt/ScreenshotEmptyStateContent_Dark_d19fbf1f_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/ConnectionsScreenshotTestsKt/ScreenshotEmptyStateContent_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/ConnectionsScreenshotTestsKt/ScreenshotEmptyStateContent_Light_b29dc7a7_0.png
new file mode 100644
index 0000000000..322ac11620
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/ConnectionsScreenshotTestsKt/ScreenshotEmptyStateContent_Light_b29dc7a7_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/ConnectionsScreenshotTestsKt/ScreenshotTransportFilterChips_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/ConnectionsScreenshotTestsKt/ScreenshotTransportFilterChips_Dark_d19fbf1f_0.png
new file mode 100644
index 0000000000..7393ba75bc
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/ConnectionsScreenshotTestsKt/ScreenshotTransportFilterChips_Dark_d19fbf1f_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/ConnectionsScreenshotTestsKt/ScreenshotTransportFilterChips_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/ConnectionsScreenshotTestsKt/ScreenshotTransportFilterChips_Light_b29dc7a7_0.png
new file mode 100644
index 0000000000..239d970acd
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/ConnectionsScreenshotTestsKt/ScreenshotTransportFilterChips_Light_b29dc7a7_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/FirmwareScreenshotTestsKt/ScreenshotFirmwareChecking_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/FirmwareScreenshotTestsKt/ScreenshotFirmwareChecking_Dark_d19fbf1f_0.png
new file mode 100644
index 0000000000..6cc3826f0c
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/FirmwareScreenshotTestsKt/ScreenshotFirmwareChecking_Dark_d19fbf1f_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/FirmwareScreenshotTestsKt/ScreenshotFirmwareChecking_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/FirmwareScreenshotTestsKt/ScreenshotFirmwareChecking_Light_b29dc7a7_0.png
new file mode 100644
index 0000000000..6a60c726dd
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/FirmwareScreenshotTestsKt/ScreenshotFirmwareChecking_Light_b29dc7a7_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/FirmwareScreenshotTestsKt/ScreenshotFirmwareDisclaimer_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/FirmwareScreenshotTestsKt/ScreenshotFirmwareDisclaimer_Dark_d19fbf1f_0.png
new file mode 100644
index 0000000000..53af1fb892
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/FirmwareScreenshotTestsKt/ScreenshotFirmwareDisclaimer_Dark_d19fbf1f_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/FirmwareScreenshotTestsKt/ScreenshotFirmwareDisclaimer_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/FirmwareScreenshotTestsKt/ScreenshotFirmwareDisclaimer_Light_b29dc7a7_0.png
new file mode 100644
index 0000000000..322df1f5d5
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/FirmwareScreenshotTestsKt/ScreenshotFirmwareDisclaimer_Light_b29dc7a7_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/FirmwareScreenshotTestsKt/ScreenshotFirmwareError_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/FirmwareScreenshotTestsKt/ScreenshotFirmwareError_Dark_d19fbf1f_0.png
new file mode 100644
index 0000000000..911520c688
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/FirmwareScreenshotTestsKt/ScreenshotFirmwareError_Dark_d19fbf1f_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/FirmwareScreenshotTestsKt/ScreenshotFirmwareError_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/FirmwareScreenshotTestsKt/ScreenshotFirmwareError_Light_b29dc7a7_0.png
new file mode 100644
index 0000000000..5ddb4d376d
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/FirmwareScreenshotTestsKt/ScreenshotFirmwareError_Light_b29dc7a7_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/FirmwareScreenshotTestsKt/ScreenshotFirmwareSuccess_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/FirmwareScreenshotTestsKt/ScreenshotFirmwareSuccess_Dark_d19fbf1f_0.png
new file mode 100644
index 0000000000..e28d5ddd84
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/FirmwareScreenshotTestsKt/ScreenshotFirmwareSuccess_Dark_d19fbf1f_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/FirmwareScreenshotTestsKt/ScreenshotFirmwareSuccess_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/FirmwareScreenshotTestsKt/ScreenshotFirmwareSuccess_Light_b29dc7a7_0.png
new file mode 100644
index 0000000000..d6ac649ffa
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/FirmwareScreenshotTestsKt/ScreenshotFirmwareSuccess_Light_b29dc7a7_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/FirmwareScreenshotTestsKt/ScreenshotFirmwareVerifying_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/FirmwareScreenshotTestsKt/ScreenshotFirmwareVerifying_Dark_d19fbf1f_0.png
new file mode 100644
index 0000000000..4a7c0d6e16
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/FirmwareScreenshotTestsKt/ScreenshotFirmwareVerifying_Dark_d19fbf1f_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/FirmwareScreenshotTestsKt/ScreenshotFirmwareVerifying_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/FirmwareScreenshotTestsKt/ScreenshotFirmwareVerifying_Light_b29dc7a7_0.png
new file mode 100644
index 0000000000..2795e38578
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/FirmwareScreenshotTestsKt/ScreenshotFirmwareVerifying_Light_b29dc7a7_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/IntroScreenshotTestsKt/ScreenshotWelcomeScreen_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/IntroScreenshotTestsKt/ScreenshotWelcomeScreen_Dark_d19fbf1f_0.png
new file mode 100644
index 0000000000..b45934b6fa
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/IntroScreenshotTestsKt/ScreenshotWelcomeScreen_Dark_d19fbf1f_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/IntroScreenshotTestsKt/ScreenshotWelcomeScreen_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/IntroScreenshotTestsKt/ScreenshotWelcomeScreen_Light_b29dc7a7_0.png
new file mode 100644
index 0000000000..effb2699c1
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/IntroScreenshotTestsKt/ScreenshotWelcomeScreen_Light_b29dc7a7_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/MessagingScreenshotTestsKt/ScreenshotEditQuickChatDialog_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/MessagingScreenshotTestsKt/ScreenshotEditQuickChatDialog_Dark_d19fbf1f_0.png
new file mode 100644
index 0000000000..abe5295bfb
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/MessagingScreenshotTestsKt/ScreenshotEditQuickChatDialog_Dark_d19fbf1f_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/MessagingScreenshotTestsKt/ScreenshotEditQuickChatDialog_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/MessagingScreenshotTestsKt/ScreenshotEditQuickChatDialog_Light_b29dc7a7_0.png
new file mode 100644
index 0000000000..7fc971ce6e
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/MessagingScreenshotTestsKt/ScreenshotEditQuickChatDialog_Light_b29dc7a7_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/MessagingScreenshotTestsKt/ScreenshotMessageInput_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/MessagingScreenshotTestsKt/ScreenshotMessageInput_Dark_d19fbf1f_0.png
new file mode 100644
index 0000000000..8c666b4efd
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/MessagingScreenshotTestsKt/ScreenshotMessageInput_Dark_d19fbf1f_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/MessagingScreenshotTestsKt/ScreenshotMessageInput_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/MessagingScreenshotTestsKt/ScreenshotMessageInput_Light_b29dc7a7_0.png
new file mode 100644
index 0000000000..4054990a14
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/MessagingScreenshotTestsKt/ScreenshotMessageInput_Light_b29dc7a7_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/MessagingScreenshotTestsKt/ScreenshotQuickChatItem_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/MessagingScreenshotTestsKt/ScreenshotQuickChatItem_Dark_d19fbf1f_0.png
new file mode 100644
index 0000000000..caa2c47e30
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/MessagingScreenshotTestsKt/ScreenshotQuickChatItem_Dark_d19fbf1f_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/MessagingScreenshotTestsKt/ScreenshotQuickChatItem_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/MessagingScreenshotTestsKt/ScreenshotQuickChatItem_Light_b29dc7a7_0.png
new file mode 100644
index 0000000000..9418710db3
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/MessagingScreenshotTestsKt/ScreenshotQuickChatItem_Light_b29dc7a7_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/MessagingScreenshotTestsKt/ScreenshotReactionItem_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/MessagingScreenshotTestsKt/ScreenshotReactionItem_Dark_d19fbf1f_0.png
new file mode 100644
index 0000000000..c75d2f0189
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/MessagingScreenshotTestsKt/ScreenshotReactionItem_Dark_d19fbf1f_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/MessagingScreenshotTestsKt/ScreenshotReactionItem_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/MessagingScreenshotTestsKt/ScreenshotReactionItem_Light_b29dc7a7_0.png
new file mode 100644
index 0000000000..8e795c97af
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/MessagingScreenshotTestsKt/ScreenshotReactionItem_Light_b29dc7a7_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotDeviceActionsLocal_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotDeviceActionsLocal_Dark_d19fbf1f_0.png
new file mode 100644
index 0000000000..ad154eb5d4
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotDeviceActionsLocal_Dark_d19fbf1f_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotDeviceActionsLocal_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotDeviceActionsLocal_Light_b29dc7a7_0.png
new file mode 100644
index 0000000000..d5f6a001db
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotDeviceActionsLocal_Light_b29dc7a7_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotDeviceActionsRemote_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotDeviceActionsRemote_Dark_d19fbf1f_0.png
new file mode 100644
index 0000000000..512e7a3381
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotDeviceActionsRemote_Dark_d19fbf1f_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotDeviceActionsRemote_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotDeviceActionsRemote_Light_b29dc7a7_0.png
new file mode 100644
index 0000000000..dfeba52858
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotDeviceActionsRemote_Light_b29dc7a7_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotDeviceMetricsCard_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotDeviceMetricsCard_Dark_d19fbf1f_0.png
new file mode 100644
index 0000000000..bd01926438
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotDeviceMetricsCard_Dark_d19fbf1f_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotDeviceMetricsCard_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotDeviceMetricsCard_Light_b29dc7a7_0.png
new file mode 100644
index 0000000000..bd01926438
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotDeviceMetricsCard_Light_b29dc7a7_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotEnvironmentMetricsContent_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotEnvironmentMetricsContent_Dark_d19fbf1f_0.png
new file mode 100644
index 0000000000..bd01926438
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotEnvironmentMetricsContent_Dark_d19fbf1f_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotEnvironmentMetricsContent_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotEnvironmentMetricsContent_Light_b29dc7a7_0.png
new file mode 100644
index 0000000000..bd01926438
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotEnvironmentMetricsContent_Light_b29dc7a7_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotLegend_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotLegend_Dark_d19fbf1f_0.png
new file mode 100644
index 0000000000..39a089614f
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotLegend_Dark_d19fbf1f_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotLegend_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotLegend_Light_b29dc7a7_0.png
new file mode 100644
index 0000000000..cb0ace90ca
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotLegend_Light_b29dc7a7_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentLoading_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentLoading_Dark_d19fbf1f_0.png
new file mode 100644
index 0000000000..1c711b83f9
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentLoading_Dark_d19fbf1f_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentLoading_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentLoading_Light_b29dc7a7_0.png
new file mode 100644
index 0000000000..539f900f7e
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentLoading_Light_b29dc7a7_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentLocal_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentLocal_Dark_d19fbf1f_0.png
new file mode 100644
index 0000000000..c939e54530
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentLocal_Dark_d19fbf1f_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentLocal_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentLocal_Light_b29dc7a7_0.png
new file mode 100644
index 0000000000..c556cd9f98
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentLocal_Light_b29dc7a7_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentRemote_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentRemote_Dark_d19fbf1f_0.png
new file mode 100644
index 0000000000..13d5b6023a
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentRemote_Dark_d19fbf1f_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentRemote_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentRemote_Light_b29dc7a7_0.png
new file mode 100644
index 0000000000..cc571c3b1d
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentRemote_Light_b29dc7a7_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailsSection_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailsSection_Dark_d19fbf1f_0.png
new file mode 100644
index 0000000000..632f6bcc6b
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailsSection_Dark_d19fbf1f_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailsSection_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailsSection_Light_b29dc7a7_0.png
new file mode 100644
index 0000000000..1b2748a520
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailsSection_Light_b29dc7a7_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotPositionInlineContent_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotPositionInlineContent_Dark_d19fbf1f_0.png
new file mode 100644
index 0000000000..a89b64538f
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotPositionInlineContent_Dark_d19fbf1f_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotPositionInlineContent_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotPositionInlineContent_Light_b29dc7a7_0.png
new file mode 100644
index 0000000000..5ba415233b
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotPositionInlineContent_Light_b29dc7a7_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotTelemetricActionsSectionEmpty_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotTelemetricActionsSectionEmpty_Dark_d19fbf1f_0.png
new file mode 100644
index 0000000000..de0d571dd1
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotTelemetricActionsSectionEmpty_Dark_d19fbf1f_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotTelemetricActionsSectionEmpty_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotTelemetricActionsSectionEmpty_Light_b29dc7a7_0.png
new file mode 100644
index 0000000000..9098a0b845
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotTelemetricActionsSectionEmpty_Light_b29dc7a7_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotTelemetricActionsSection_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotTelemetricActionsSection_Dark_d19fbf1f_0.png
new file mode 100644
index 0000000000..9bc872a56f
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotTelemetricActionsSection_Dark_d19fbf1f_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotTelemetricActionsSection_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotTelemetricActionsSection_Light_b29dc7a7_0.png
new file mode 100644
index 0000000000..9bfd4f8d87
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotTelemetricActionsSection_Light_b29dc7a7_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotAppInfoSection_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotAppInfoSection_Dark_d19fbf1f_0.png
new file mode 100644
index 0000000000..dcd9c21410
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotAppInfoSection_Dark_d19fbf1f_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotAppInfoSection_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotAppInfoSection_Light_b29dc7a7_0.png
new file mode 100644
index 0000000000..ff78aa0353
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotAppInfoSection_Light_b29dc7a7_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotAppearanceSection_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotAppearanceSection_Dark_d19fbf1f_0.png
new file mode 100644
index 0000000000..0967e1dace
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotAppearanceSection_Dark_d19fbf1f_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotAppearanceSection_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotAppearanceSection_Light_b29dc7a7_0.png
new file mode 100644
index 0000000000..aecd2b6208
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotAppearanceSection_Light_b29dc7a7_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotPersistenceSection_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotPersistenceSection_Dark_d19fbf1f_0.png
new file mode 100644
index 0000000000..d619c670c0
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotPersistenceSection_Dark_d19fbf1f_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotPersistenceSection_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotPersistenceSection_Light_b29dc7a7_0.png
new file mode 100644
index 0000000000..f2f5aac537
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotPersistenceSection_Light_b29dc7a7_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotConnectedFailed_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotConnectedFailed_Dark_d19fbf1f_0.png
new file mode 100644
index 0000000000..16a07222b9
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotConnectedFailed_Dark_d19fbf1f_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotConnectedFailed_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotConnectedFailed_Light_b29dc7a7_0.png
new file mode 100644
index 0000000000..b0504a8745
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotConnectedFailed_Light_b29dc7a7_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotConnectedSuccess_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotConnectedSuccess_Dark_d19fbf1f_0.png
new file mode 100644
index 0000000000..e5cea75cf1
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotConnectedSuccess_Dark_d19fbf1f_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotConnectedSuccess_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotConnectedSuccess_Light_b29dc7a7_0.png
new file mode 100644
index 0000000000..fe4b685507
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotConnectedSuccess_Light_b29dc7a7_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotConnectedWithNetworks_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotConnectedWithNetworks_Dark_d19fbf1f_0.png
new file mode 100644
index 0000000000..d2fe80311d
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotConnectedWithNetworks_Dark_d19fbf1f_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotConnectedWithNetworks_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotConnectedWithNetworks_Light_b29dc7a7_0.png
new file mode 100644
index 0000000000..b0d27ce323
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotConnectedWithNetworks_Light_b29dc7a7_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotDeviceFound_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotDeviceFound_Dark_d19fbf1f_0.png
new file mode 100644
index 0000000000..77b2ba9810
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotDeviceFound_Dark_d19fbf1f_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotDeviceFound_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotDeviceFound_Light_b29dc7a7_0.png
new file mode 100644
index 0000000000..6f499bd453
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotDeviceFound_Light_b29dc7a7_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotMpwrdDisclaimerBanner_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotMpwrdDisclaimerBanner_Dark_d19fbf1f_0.png
new file mode 100644
index 0000000000..3365e43a4b
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotMpwrdDisclaimerBanner_Dark_d19fbf1f_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotMpwrdDisclaimerBanner_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotMpwrdDisclaimerBanner_Light_b29dc7a7_0.png
new file mode 100644
index 0000000000..c06ea1127c
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotMpwrdDisclaimerBanner_Light_b29dc7a7_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotNetworkRow_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotNetworkRow_Dark_d19fbf1f_0.png
new file mode 100644
index 0000000000..0a723868ae
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotNetworkRow_Dark_d19fbf1f_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotNetworkRow_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotNetworkRow_Light_b29dc7a7_0.png
new file mode 100644
index 0000000000..5fd1543200
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotNetworkRow_Light_b29dc7a7_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotProvisionStatusCardSuccess_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotProvisionStatusCardSuccess_Dark_d19fbf1f_0.png
new file mode 100644
index 0000000000..57ffb37861
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotProvisionStatusCardSuccess_Dark_d19fbf1f_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotProvisionStatusCardSuccess_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotProvisionStatusCardSuccess_Light_b29dc7a7_0.png
new file mode 100644
index 0000000000..187fe1e81e
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotProvisionStatusCardSuccess_Light_b29dc7a7_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotScanningBle_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotScanningBle_Dark_d19fbf1f_0.png
new file mode 100644
index 0000000000..454bd623f9
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotScanningBle_Dark_d19fbf1f_0.png differ
diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotScanningBle_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotScanningBle_Light_b29dc7a7_0.png
new file mode 100644
index 0000000000..9e45f1cdb1
Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTestsKt/ScreenshotScanningBle_Light_b29dc7a7_0.png differ
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 9235220424..7bdc9d100d 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -122,5 +122,6 @@ if (!desktopOnly) {
":core:api",
":core:barcode",
":feature:widget",
+ ":screenshot-tests",
)
}
diff --git a/specs/018-compose-screenshot-testing/checklists/implementation.md b/specs/018-compose-screenshot-testing/checklists/implementation.md
new file mode 100644
index 0000000000..73abb962ff
--- /dev/null
+++ b/specs/018-compose-screenshot-testing/checklists/implementation.md
@@ -0,0 +1,108 @@
+# Implementation Quality Checklist: Compose Preview Screenshot Testing
+
+**Purpose**: Deep pre-push self-check for the author across KMP architecture, build integration, and preview coverage
+**Created**: 2026-05-08
+**Feature**: [spec.md](../spec.md) | [plan.md](../plan.md) | [tasks.md](../tasks.md)
+
+## Requirement Completeness
+
+- [x] CHK001 - Are Gradle plugin configuration requirements specified for both `gradle.properties` AND module-level `experimentalProperties`? [Completeness, Spec Β§FR-008, Β§FR-017]
+- [x] CHK002 - Are version catalog entries defined with exact group/name/version coordinates for both the CST plugin and the validation API library? [Completeness, Spec Β§FR-007]
+- [x] CHK003 - Are the `screenshotTestImplementation` dependency configurations explicitly listed (validation-api + ui-tooling)? [Completeness, Spec Β§FR-006]
+- [x] CHK004 - Is the `settings.gradle.kts` inclusion specified with the conditional `desktopOnly` exclusion guard? [Completeness, Spec Β§FR-009]
+- [x] CHK005 - Are all 8 eligible modules from SC-002 explicitly addressed in the tasks β either with existing preview wrappers or new preview creation tasks? [Completeness, Spec Β§SC-002]
+- [x] CHK006 - Are docs pipeline integration requirements defined (copy task, manifest, target directory, .gitignore)? [Completeness, Spec Β§FR-013]
+- [x] CHK007 - Are CI workflow requirements specified (validation step, artifact upload on failure, correct `if:` condition)? [Completeness, Spec Β§FR-014]
+
+## Requirement Clarity
+
+- [x] CHK008 - Is the dual-annotation pattern (`@PreviewTest` + `@Preview`/`@PreviewLightDark`) unambiguously stated for all wrapper tasks? [Clarity, Spec Β§FR-006]
+- [x] CHK009 - Is the `imageDifferenceThreshold` value specified with its unit and rationale (0.0005f = 0.05% for cross-OS JDK font differences)? [Clarity, Plan Β§Technical Context]
+- [x] CHK010 - Is the reference image storage path (`screenshotTestDebug/reference/`) consistently used across spec, plan, data-model, and quickstart? [Clarity]
+- [x] CHK011 - Is "primary components" defined with concrete composable names for each module needing new previews? [Clarity, Spec Β§FR-012]
+- [x] CHK012 - Is the `namespace` for the screenshot-tests module explicitly specified (`org.meshtastic.screenshot.tests`)? [Clarity, Plan Β§data-model]
+- [x] CHK013 - Is the Gradle task naming convention unambiguous β `validateFdroidDebugScreenshotTest` consistently used rather than the generic `validateDebugScreenshotTest`? [Clarity, Spec Β§FR-014]
+
+## Requirement Consistency
+
+- [x] CHK014 - Are the module exclusion lists consistent between FR-012 (spec), the deferred modules table (data-model), and the Dependencies section (tasks)? [Consistency, Spec Β§FR-012]
+- [x] CHK015 - Is the SC-002 eligible module list (8 modules) consistent with FR-012's inclusion scope and the tasks that create wrappers? [Consistency, Spec Β§SC-002]
+- [x] CHK016 - Are the convention plugins applied to `screenshot-tests` consistent across plan (`meshtastic.detekt` + `meshtastic.spotless`), spec (FR-010), and task T003? [Consistency]
+- [x] CHK017 - Does the plan's Project Structure tree match the actual file paths referenced in tasks T012, T013, T019-T027a, T039? [Consistency]
+- [x] CHK018 - Are the Constitution Check gates (plan.md pre-design and post-design) consistent with each other and with the verification commands in T047? [Consistency]
+
+## KMP Architecture Compliance
+
+- [x] CHK019 - Is it documented that `screenshot-tests/` is intentionally Android-only and does NOT use `meshtastic.kmp.*` convention plugins? [Completeness, Plan Β§Complexity Tracking]
+- [x] CHK020 - Are requirements clear that NO `java.*` or `android.*` imports are added to any `commonMain` source set as part of this feature? [Completeness, Plan Β§Constitution Check I]
+- [x] CHK021 - Is the visibility convention (`internal`, not `private`) specified for preview composables that need cross-module access? [Clarity, Spec Β§FR-004]
+- [x] CHK022 - Are the KMP compile verification commands specified for audited modules (`compileKotlinJvm` for KMP modules, not `compileFdroidDebugKotlin`)? [Clarity, Tasks Β§T010]
+- [x] CHK023 - Is it clear that preview composables live in `commonMain` (not `androidMain`) so they compile on all KMP targets? [Clarity, Spec Β§FR-004, Β§FR-005]
+- [x] CHK024 - Are the modules with `androidMain`-only previews identified for migration to `commonMain` (`feature:settings`, `feature:intro`)? [Coverage, Spec Β§FR-012]
+- [x] CHK025 - Is the justification for the Android-only module documented in Complexity Tracking with the rejected alternative? [Completeness, Plan Β§Complexity Tracking]
+
+## Build Integration Correctness
+
+- [x] CHK026 - Are both experimental flags documented (gradle.properties global + module-level `experimentalProperties`)? [Completeness, Spec Β§FR-008, Β§FR-017]
+- [x] CHK027 - Is the plugin application order specified (`com.android.library` before `com.android.compose.screenshot`)? [Clarity, Gap]
+- [x] CHK028 - Are `compileSdk` and `minSdk` sourcing requirements specified (from `config.properties` via convention, matching the rest of the project)? [Clarity, Gap]
+- [x] CHK029 - Is the compose compiler plugin application requirement documented for the screenshot-tests module? [Completeness, Tasks Β§T003]
+- [x] CHK030 - Are the `screenshotTestImplementation` vs `implementation` dependency scopes correctly distinguished in requirements? [Clarity, Spec Β§FR-003, Β§FR-006]
+- [x] CHK031 - Is the minimal `AndroidManifest.xml` requirement documented (empty manifest, namespace from build.gradle.kts)? [Completeness, Tasks Β§T004]
+- [x] CHK032 - Is the CI workflow step ordering specified (build before validate, validate before artifact upload, `if: failure()` on upload)? [Completeness, Tasks Β§T035-T038]
+
+## Preview Convention Coverage
+
+- [x] CHK033 - Are `AppTheme` wrapping requirements defined for ALL preview composables, not just new ones? [Coverage, Spec Β§FR-005]
+- [x] CHK034 - Are the specific `core:ui` component files needing `AppTheme` fixes enumerated? [Completeness, Tasks Β§T008]
+- [x] CHK035 - Are the specific `feature:node` files with `private` visibility listed with function count? [Completeness, Tasks Β§T009]
+- [x] CHK036 - Is the synthetic sample data convention explicit β hardcoded strings or `core/resources` string resources, never production data? [Clarity, Spec Β§FR-016]
+- [x] CHK037 - Are `@PreviewParameter` provider-based previews addressed (e.g., `SignalInfo`, `MaterialBatteryInfo`, `MaterialBluetoothSignalInfo` use `PreviewParameterProvider`)? [Coverage, Gap]
+- [x] CHK038 - Are `CompositionLocal` edge cases addressed β do requirements specify that previews must supply defaults or use `LocalInspectionMode` guards? [Edge Case, Spec Β§Edge Cases]
+- [x] CHK039 - Are Compose resource dependency requirements defined β `screenshot-tests` must depend on modules owning resources used by previews? [Edge Case, Spec Β§Edge Cases]
+
+## Scenario Coverage
+
+- [x] CHK040 - Are requirements defined for what happens when a contributor renames a `@PreviewTest` function (reference images invalidated)? [Coverage, Edge Case]
+- [x] CHK041 - Are requirements defined for orphaned reference images (preview deleted but PNG remains)? [Coverage, Edge Case]
+- [x] CHK042 - Are cross-OS rendering difference requirements specified (threshold value, CI-generated references as source of truth)? [Coverage, Spec Β§Assumptions]
+- [x] CHK043 - Is the recovery flow defined when CST plugin version becomes incompatible with AGP? [Coverage, Spec Β§Edge Cases]
+- [x] CHK044 - Are requirements defined for the case where a new `@PreviewTest` wrapper is added but `updateScreenshotTest` is not run? [Coverage, Spec Β§Edge Cases]
+- [x] CHK045 - Is the scenario addressed where a preview depends on a `CompositionLocal` not provided in the screenshot test context? [Coverage, Spec Β§Edge Cases]
+
+## Acceptance Criteria Quality
+
+- [x] CHK046 - Is the 60-second performance budget (SC-001) measurable and is there a task to validate it? [Measurability, Spec Β§SC-001]
+- [x] CHK047 - Is the 80% module coverage threshold (SC-002) calculable with the explicit denominator of 8 eligible modules? [Measurability, Spec Β§SC-002]
+- [x] CHK048 - Is the "under 10 minutes" quickstart criterion (SC-005) testable β does a task exist to walkthrough the quickstart? [Measurability, Spec Β§SC-005, Tasks Β§T049]
+- [x] CHK049 - Is SC-006 (spotlessCheck + detekt pass) verified by an explicit task with the correct Gradle commands? [Measurability, Spec Β§SC-006, Tasks Β§T047-T048]
+
+## Non-Functional Requirements
+
+- [x] CHK050 - Are privacy requirements specified for reference image content (synthetic data only, no PII in rendered screenshots)? [Privacy, Spec Β§FR-005, Β§FR-016]
+- [x] CHK051 - Are lint/formatting requirements specified for the screenshot test source set (not just the module β does `screenshotTest` code get formatted by spotless)? [Consistency, Spec Β§FR-010]
+- [x] CHK052 - Is the version control strategy for reference images documented (commit PNGs, reviewable in PR diffs)? [Completeness, Spec Β§FR-015]
+- [x] CHK053 - Are the local verification commands documented and do they include the screenshot validation task? [Completeness, Plan Β§Constitution Check VI]
+
+## Dependencies & Assumptions
+
+- [x] CHK054 - Is the AGP 9.2.1 + CST alpha14 compatibility assumption documented and validated? [Assumption, Spec Β§Assumptions]
+- [x] CHK055 - Is the CMP 1.11+ `@Preview` package alignment assumption documented (same `androidx.compose.ui.tooling.preview` as Jetpack)? [Assumption, Research Β§2]
+- [x] CHK056 - Is the dependency on spec 003 (docs pipeline) scoped β does this spec define only the generation mechanism, leaving consumption to spec 003? [Dependency, Spec Β§Assumptions]
+- [x] CHK057 - Is the assumption that previews require no ViewModel/DI/network access documented? [Assumption, Spec Β§Assumptions]
+
+## Ambiguities & Gaps
+
+- [x] CHK058 - Is the docs-screenshots manifest format defined precisely (glob syntax, one pattern per line, comment support)? [Ambiguity, Tasks Β§T032]
+- [x] CHK059 - Is the `copyDocsScreenshots` task's file renaming strategy specified (how CST hash-based names map to stable doc-friendly names)? [Ambiguity, Tasks Β§T031]
+- [x] CHK060 - Is it specified whether the `screenshot-tests` module needs product flavors (fdroid/google) or just debug/release build types? [Gap]
+- [x] CHK061 - Is it clear whether T037 (reusable workflow check) supersedes T035-T036 or supplements them? [Ambiguity, Tasks Β§T037]
+- [x] CHK062 - Are the `@PreviewTest` wrapper naming conventions defined (e.g., `{PreviewName}ScreenshotTest`)? [Gap, Data Model Β§Screenshot Test Wrapper]
+
+## Notes
+
+- This checklist covers all three requested domains (KMP architecture, build integration, preview coverage) at deep rigor
+- Audience: author pre-push self-check β items are ordered for sequential validation during implementation
+- 62 items total across 10 quality dimensions
+- Items reference specific spec sections, plan sections, and task IDs for traceability
+- CHK027, CHK028, CHK037, CHK060, CHK062 are marked `[Gap]` β these represent areas where requirements may need to be added
diff --git a/specs/018-compose-screenshot-testing/checklists/requirements.md b/specs/018-compose-screenshot-testing/checklists/requirements.md
new file mode 100644
index 0000000000..b311e3c0d3
--- /dev/null
+++ b/specs/018-compose-screenshot-testing/checklists/requirements.md
@@ -0,0 +1,37 @@
+# Specification Quality Checklist: Compose Preview Screenshot Testing
+
+**Purpose**: Validate specification completeness and quality before proceeding to planning
+**Created**: 2026-05-08
+**Feature**: [spec.md](../spec.md)
+
+## Content Quality
+
+- [x] No implementation details (languages, frameworks, APIs)
+- [x] Focused on user value and business needs
+- [x] Written for non-technical stakeholders
+- [x] All mandatory sections completed
+
+## Requirement Completeness
+
+- [x] No [NEEDS CLARIFICATION] markers remain
+- [x] Requirements are testable and unambiguous
+- [x] Success criteria are measurable
+- [x] Success criteria are technology-agnostic (no implementation details)
+- [x] All acceptance scenarios are defined
+- [x] Edge cases are identified
+- [x] Scope is clearly bounded
+- [x] Dependencies and assumptions identified
+
+## Feature Readiness
+
+- [x] All functional requirements have clear acceptance criteria
+- [x] User scenarios cover primary flows
+- [x] Feature meets measurable outcomes defined in Success Criteria
+- [x] No implementation details leak into specification
+
+## Notes
+
+- FR-001 references the specific plugin name (`com.android.compose.screenshot`) which is a technology reference, but this is acceptable because the entire feature IS about integrating a specific tool β the tool name is part of the business requirement, not an implementation detail.
+- FR-002 references `screenshot-tests/` module name β this is a structural requirement, not an implementation detail. The spec intentionally specifies module placement to enforce architectural boundaries.
+- SC-001 includes a 60-second time bound which is measurable and user-facing (developer experience).
+- All items pass. Spec is ready for `/speckit.plan` or `/speckit.tasks`.
diff --git a/specs/018-compose-screenshot-testing/data-model.md b/specs/018-compose-screenshot-testing/data-model.md
new file mode 100644
index 0000000000..6a1779d471
--- /dev/null
+++ b/specs/018-compose-screenshot-testing/data-model.md
@@ -0,0 +1,150 @@
+# Data Model: Compose Preview Screenshot Testing
+
+**Feature**: 018-compose-screenshot-testing
+**Date**: 2026-05-08
+
+## Entities
+
+This feature does not introduce database entities or domain models. The "data" consists of file-system artifacts managed by the CST plugin and Gradle build system.
+
+### Preview Composable
+
+A `@Composable` function annotated with `@Preview` or `@PreviewLightDark` in a KMP module's `commonMain` source set.
+
+| Field | Type | Description |
+|-------|------|-------------|
+| Function name | Kotlin function | Unique within its file; naming convention `*Preview` or in `*Previews.kt` |
+| Visibility | `internal` or `public` | Must NOT be `private` β needs to be importable by `screenshot-tests` module |
+| Annotations | `@Preview` / `@PreviewLightDark` | From `androidx.compose.ui.tooling.preview` (CMP 1.11+) |
+| Theme wrapper | `AppTheme { ... }` | Required β ensures M3 theming in reference images |
+| Sample data | Hardcoded synthetic | Never real user data, PII, or location coordinates |
+| Module | KMP module path | e.g. `core:ui`, `feature:messaging` |
+
+**Validation rules**:
+- Function must be annotated with at least one preview annotation
+- Content must be wrapped in `AppTheme`
+- No ViewModel, DI, or network dependencies
+- Must compile on all KMP targets (lives in `commonMain`)
+
+### Screenshot Test Wrapper
+
+A `@Composable` function in the `screenshotTest` source set of the `screenshot-tests` module.
+
+| Field | Type | Description |
+|-------|------|-------------|
+| Function name | Kotlin function | Convention: `{PreviewName}ScreenshotTest` |
+| Annotations | `@PreviewTest` + `@Preview`/`@PreviewLightDark` | `@PreviewTest` from `com.android.tools.screenshot` |
+| Body | Composable call | Delegates to the target preview composable from a KMP module |
+
+**Validation rules**:
+- Must have `@PreviewTest` annotation (required since CST alpha10)
+- Must also have `@Preview` or `@PreviewLightDark` (tells CST what configurations to render)
+- Renaming the function invalidates existing reference images
+
+### Reference Image
+
+A PNG file generated by the CST `update{Variant}ScreenshotTest` task.
+
+| Field | Type | Description |
+|-------|------|-------------|
+| File name | String | `{package}.{FunctionName}_{hash1}_{hash2}_{index}.png` |
+| Location | File path | `screenshot-tests/src/screenshotTestDebug/reference/` |
+| Format | PNG | Lossless raster image |
+| Variant | Build variant | e.g. `fdroidDebug`, `debug` |
+
+**Validation rules**:
+- Must be committed to version control
+- Generated on a consistent OS/JDK to avoid rendering differences
+- Validated by `validate{Variant}ScreenshotTest` against current rendered output
+
+### Diff Report
+
+An HTML report generated by the CST `validate{Variant}ScreenshotTest` task when validation fails.
+
+| Field | Type | Description |
+|-------|------|-------------|
+| Location | File path | `screenshot-tests/build/reports/screenshotTest/preview/{variant}/index.html` |
+| Content | HTML | Side-by-side reference/actual/diff images with percentage difference |
+| CI artifact | Uploaded | Available as GitHub Actions build artifact on failure |
+
+## Relationships
+
+```
+Preview Composable (commonMain)
+ β imported by
+Screenshot Test Wrapper (screenshotTest source set)
+ β rendered by CST plugin β generates
+Reference Image (version-controlled PNG)
+ β compared by validate task β produces on diff
+Diff Report (HTML, CI artifact)
+ β selected images copied by
+Docs Asset (consumed by spec 003 docs pipeline)
+```
+
+## State Transitions
+
+Reference images have a simple lifecycle:
+
+```
+[Not Exists] β update task β [Generated] β commit β [Tracked]
+[Tracked] β UI changes β validate fails β [Stale]
+[Stale] β update task β [Updated] β commit β [Tracked]
+[Tracked] β preview deleted β [Orphaned] β manual cleanup β [Removed]
+```
+
+## Version Catalog Entries
+
+New entries required in `gradle/libs.versions.toml`:
+
+```toml
+[versions]
+compose-screenshot = "0.0.1-alpha14"
+
+[libraries]
+screenshot-validation-api = { group = "com.android.tools.screenshot", name = "screenshot-validation-api", version.ref = "compose-screenshot" }
+
+[plugins]
+compose-screenshot = { id = "com.android.compose.screenshot", version.ref = "compose-screenshot" }
+```
+
+## Configuration Properties
+
+| Property | File | Value |
+|----------|------|-------|
+| `android.experimental.enableScreenshotTest` | `gradle.properties` | `true` |
+| `experimentalProperties["android.experimental.enableScreenshotTest"]` | `screenshot-tests/build.gradle.kts` | `true` |
+| `imageDifferenceThreshold` | `screenshot-tests/build.gradle.kts` | `0.0005f` |
+| `namespace` | `screenshot-tests/build.gradle.kts` | `org.meshtastic.screenshot.tests` |
+
+## Existing Preview Inventory
+
+### Modules with existing previews (require audit)
+
+| Module | Preview count | Files | Key issues |
+|--------|--------------|-------|------------|
+| `core:ui` | ~40+ | ~25 component files + `AlertPreviews.kt` | Mixed theme wrapping; some lack `AppTheme` |
+| `feature:messaging` | 6 | 4 files | Good β uses `AppTheme`, `@PreviewLightDark` |
+| `feature:node` | 14 | 5 files | `NodeDetailPreviews.kt` has 4 `private` previews |
+| `feature:wifi-provision` | 19 | 2 files | Excellent coverage |
+| `feature:settings` | 3 | 3 files (all `androidMain`) | Need `commonMain` previews |
+| `feature:intro` | 1 | 1 file (`androidMain`) | Need `commonMain` previews |
+
+### Modules needing new previews
+
+| Module | Primary components to preview |
+|--------|------------------------------|
+| `feature:connections` | `DeviceListItem`, `ConnectingDeviceInfo`, `DisconnectButton`, `DeviceList` |
+| `feature:firmware` | `FirmwareUpdateScreen` and sub-components |
+| `feature:settings` (commonMain) | Settings sections currently only previewed in `androidMain` |
+| `feature:intro` (commonMain) | Welcome/onboarding screen composables currently only previewed in `androidMain` |
+
+### Modules deferred
+
+| Module | Reason |
+|--------|--------|
+| `feature:map` | Platform-specific map renderers don't work in CST preview context |
+| `feature:widget` | Glance composables are incompatible with CST |
+| `feature:discovery` | No UI composables |
+| `feature:metrics` | No UI composables (metrics embedded in `feature:node`) |
+| `feature:auto` | No UI composables |
+| `feature:docs` | No UI composables |
diff --git a/specs/018-compose-screenshot-testing/plan.md b/specs/018-compose-screenshot-testing/plan.md
new file mode 100644
index 0000000000..f0a6675b1e
--- /dev/null
+++ b/specs/018-compose-screenshot-testing/plan.md
@@ -0,0 +1,119 @@
+# Implementation Plan: Compose Preview Screenshot Testing
+
+**Branch**: `018-compose-screenshot-testing` | **Date**: 2026-05-08 | **Spec**: [spec.md](spec.md)
+**Input**: Feature specification from `specs/018-compose-screenshot-testing/spec.md`
+
+## Summary
+
+Integrate the official Compose Preview Screenshot Testing (CST) plugin into the Meshtastic Android KMP project. A dedicated Android-only `screenshot-tests/` module imports `commonMain` preview composables from KMP feature and core modules, runs host-side JVM screenshot validation (no emulator), and feeds reference images into the docs pipeline and CI checks. Existing previews are audited for visibility/theming, and new previews are added for uncovered modules to achieve 80%+ component coverage.
+
+## Technical Context
+
+**Language/Version**: Kotlin 2.3.21, JDK 21
+**Primary Dependencies**: AGP 9.2.1, CMP 1.11.0-rc01, CST plugin 0.0.1-alpha14, screenshot-validation-api 0.0.1-alpha14
+**Storage**: N/A β reference images stored as PNGs in version control
+**Testing**: CST `validateDebugScreenshotTest` (host-side JVM, no emulator)
+**Target Platform**: Android (screenshot tests); previews authored in CMP `commonMain` (Android + Desktop)
+**Project Type**: Mobile app (KMP) with Android-only screenshot testing module
+**Performance Goals**: Validation task completes in <60 seconds for the full preview suite
+**Constraints**: CST plugin is Android-only; cannot apply to KMP modules directly
+**Scale/Scope**: ~50+ preview composables across ~10 modules, generating ~100+ reference images
+
+## Constitution Check (Pre-Design)
+
+*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
+
+- **I. Kotlin Multiplatform Core**: β
PASS β Preview composables are authored in `commonMain` using CMP `@Preview`/`@PreviewLightDark`. The `screenshot-tests/` module is intentionally Android-only (it is a test harness, not business logic). No `java.*` or `android.*` imports are added to any `commonMain` source set. Platform-specific work is isolated to the screenshot test module and CI workflow.
+
+- **II. Zero Lint Tolerance**: β
PASS β The `screenshot-tests` module will apply `meshtastic.detekt` and `meshtastic.spotless` convention plugins. Verification commands:
+ - `./gradlew :screenshot-tests:detekt`
+ - `./gradlew spotlessApply spotlessCheck`
+ - All existing module lint commands remain unchanged.
+
+- **III. Compose Multiplatform UI**: β
PASS β All preview composables use CMP `@Preview`/`@PreviewLightDark` from `androidx.compose.ui.tooling.preview` (CMP 1.11+ uses identical package to Jetpack). Previews wrap content in `AppTheme` (CMP, not Android-only). No navigation or float formatting in scope (previews are stateless component snapshots).
+
+- **IV. Privacy First**: β
PASS β Preview sample data uses hardcoded synthetic values only. No PII, location data, or cryptographic keys. No modifications to `core/proto` submodule. Reference images contain only synthetic UI renderings.
+
+- **V. Design Standards Compliance**: β
N/A β This feature does not introduce new user-facing screens. Preview composables render existing components. The previews themselves serve as a visual audit tool to verify design standards compliance.
+
+- **VI. Verify Before Push**: β
PASS β Local verification commands:
+ ```bash
+ ./gradlew spotlessApply detekt assembleDebug test allTests :screenshot-tests:validateFdroidDebugScreenshotTest
+ ```
+ Post-push CI check:
+ ```bash
+ gh pr checks <PR_NUMBER>
+ ```
+
+## Project Structure
+
+### Documentation (this feature)
+
+```text
+specs/018-compose-screenshot-testing/
+βββ plan.md # This file
+βββ research.md # Phase 0 output β technology decisions
+βββ data-model.md # Phase 1 output β entity catalog
+βββ quickstart.md # Phase 1 output β contributor guide
+βββ tasks.md # Phase 2 output (created by /speckit.tasks)
+```
+
+### Source Code (repository root)
+
+```text
+# New module
+screenshot-tests/
+βββ build.gradle.kts # Android library + CST plugin
+βββ src/
+β βββ main/
+β β βββ AndroidManifest.xml # Minimal manifest
+β βββ screenshotTest/
+β β βββ kotlin/org/meshtastic/screenshots/
+β β βββ core/ # @PreviewTest wrappers for core/ui previews
+β β β βββ AlertScreenshotTests.kt
+β β β βββ ComponentScreenshotTests.kt
+β β β βββ ...
+β β βββ feature/ # @PreviewTest wrappers for feature previews
+β β βββ MessagingScreenshotTests.kt
+β β βββ NodeScreenshotTests.kt
+β β βββ ConnectionsScreenshotTests.kt
+β β βββ SettingsScreenshotTests.kt
+β β βββ FirmwareScreenshotTests.kt
+β β βββ WifiProvisionScreenshotTests.kt
+β βββ screenshotTestDebug/
+β βββ reference/ # Git-tracked reference PNGs
+β βββ *.png
+
+# Modified files (existing)
+gradle/libs.versions.toml # + CST plugin & screenshot-validation-api
+gradle.properties # + android.experimental.enableScreenshotTest=true
+settings.gradle.kts # + include(":screenshot-tests")
+.github/workflows/pull-request.yml # + screenshot validation step
+
+# Audited preview files (existing, visibility/theme fixes)
+feature/node/src/commonMain/.../NodeDetailPreviews.kt
+core/ui/src/commonMain/.../component/*.kt # Theme wrapping consistency
+feature/settings/src/commonMain/... # New previews (commonMain)
+feature/connections/src/commonMain/... # New previews
+feature/firmware/src/commonMain/... # New previews
+feature/intro/src/commonMain/... # New previews (migrate from androidMain)
+```
+
+**Structure Decision**: The `screenshot-tests/` module sits at the repository root (same level as `app/`, `desktop/`, `core/`, `feature/`) because it is a cross-cutting test harness that spans multiple core and feature modules. It is NOT a KMP module β it is intentionally Android-only, using `com.android.library` + CST plugin.
+
+## Constitution Check (Post-Design)
+
+*Re-evaluation after Phase 1 design.*
+
+- **I. KMP Core**: β
Confirmed β no `commonMain` contamination. The `screenshot-tests/` module is Android-only test infrastructure. All preview composables remain in `commonMain`.
+- **II. Zero Lint**: β
Confirmed β `meshtastic.detekt` and `meshtastic.spotless` applied to `screenshot-tests/`.
+- **III. CMP UI**: β
Confirmed β all previews use CMP annotations. `AppTheme` is from `core/ui` (CMP).
+- **IV. Privacy**: β
Confirmed β synthetic data only. No PII in reference images.
+- **V. Design Standards**: β
N/A β no new user-facing screens.
+- **VI. Verify Before Push**: β
Confirmed β commands listed above.
+
+## Complexity Tracking
+
+| Violation | Why Needed | Simpler Alternative Rejected Because |
+|-----------|------------|-------------------------------------|
+| Android-only module in KMP project | CST plugin is Android-only; cannot apply to KMP module targets | Applying CST to `app` module couples tests to the host shell; applying to KMP modules is impossible |
diff --git a/specs/018-compose-screenshot-testing/quickstart.md b/specs/018-compose-screenshot-testing/quickstart.md
new file mode 100644
index 0000000000..12e8c14e6b
--- /dev/null
+++ b/specs/018-compose-screenshot-testing/quickstart.md
@@ -0,0 +1,114 @@
+# Quickstart: Adding Screenshot Tests
+
+**Feature**: 018-compose-screenshot-testing
+
+This guide explains how to add a new preview composable and wire it into the screenshot test suite.
+
+## Prerequisites
+
+- JDK 21, `ANDROID_HOME` set, proto submodule initialized
+- `android.experimental.enableScreenshotTest=true` in `gradle.properties`
+
+## Step 1: Create a Preview Composable
+
+Add a `@Preview` or `@PreviewLightDark` composable in your module's `commonMain`:
+
+```kotlin
+// feature/messaging/src/commonMain/kotlin/.../component/MyComponentPreviews.kt
+
+@PreviewLightDark
+@Composable
+fun MyComponentPreview() {
+ AppTheme {
+ MyComponent(
+ title = "Sample Title",
+ subtitle = "Sample subtitle text",
+ )
+ }
+}
+```
+
+**Rules**:
+- Visibility: public (no modifier) β the screenshot-tests module must import it across module boundaries (`internal` does NOT work across modules)
+- Theme: Always wrap in `AppTheme { ... }`
+- Data: Use hardcoded synthetic values β never real user data or PII
+- Dependencies: No ViewModel, DI, or network access β stateless only
+- Determinism: Avoid time-dependent or random data (e.g., `Channel.getRandomKey()`, relative timestamps like "last heard X ago") β these cause flaky diffs
+
+## Step 2: Add a Screenshot Test Wrapper
+
+Create or update a file in the screenshot-tests module:
+
+```kotlin
+// screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/feature/MessagingScreenshotTests.kt
+
+@PreviewTest
+@PreviewLightDark
+@Composable
+fun MyComponentScreenshotTest() {
+ MyComponentPreview()
+}
+```
+
+**Rules**:
+- Must have `@PreviewTest` annotation (from `com.android.tools.screenshot`)
+- Must also have the same `@Preview` or `@PreviewLightDark` as the source preview
+- Function can be `public` (default) β this is a test, not API surface
+
+## Step 3: Generate Reference Images
+
+```bash
+./gradlew :screenshot-tests:updateDebugScreenshotTest
+```
+
+Reference images are saved to `screenshot-tests/src/screenshotTestDebug/reference/`.
+
+## Step 4: Validate
+
+```bash
+./gradlew :screenshot-tests:validateDebugScreenshotTest
+```
+
+If no UI changes were made, this passes. If the rendered output differs from references, it fails and produces an HTML diff report at `screenshot-tests/build/reports/screenshotTest/preview/debug/index.html`.
+
+## Step 5: Commit Reference Images
+
+```bash
+git add screenshot-tests/src/screenshotTestDebug/reference/
+git commit -m "Add screenshot references for MyComponent"
+```
+
+Reference images must be in version control so CI can validate against them.
+
+## Updating After UI Changes
+
+If you intentionally change a component's appearance:
+
+1. Run the update task to regenerate references:
+ ```bash
+ ./gradlew :screenshot-tests:updateDebugScreenshotTest
+ ```
+2. Review the updated PNGs in `screenshot-tests/src/screenshotTestDebug/reference/`
+3. Commit the updated images
+4. Validate:
+ ```bash
+ ./gradlew :screenshot-tests:validateDebugScreenshotTest
+ ```
+
+## Common Issues
+
+**Preview not found by CST**: Ensure the preview function is public (no modifier, not `private` or `internal`) and the `screenshot-tests` module has an `implementation(project(":your:module"))` dependency.
+
+**Theme not applied**: Wrap preview content in `AppTheme { ... }`. The theme is in `core:ui`.
+
+**Reference image diff on CI but not locally**: Reference images should be generated on CI (Ubuntu) or with an identical JDK version. Minor font rendering differences between macOS and Linux are absorbed by the `imageDifferenceThreshold` (0.05%).
+
+**`@PreviewTest` not resolving**: Ensure `screenshotTestImplementation(libs.screenshot.validation.api)` is in `screenshot-tests/build.gradle.kts`.
+
+## File Naming Conventions
+
+| File type | Location | Convention |
+|-----------|----------|------------|
+| Preview composable | `{module}/src/commonMain/.../component/{Name}Previews.kt` | Group related previews in `*Previews.kt` files |
+| Screenshot test wrapper | `screenshot-tests/src/screenshotTest/.../feature/{Module}ScreenshotTests.kt` | One file per source module |
+| Reference image | `screenshot-tests/src/screenshotTestDebug/reference/*.png` | Auto-generated names (do not rename) |
diff --git a/specs/018-compose-screenshot-testing/research.md b/specs/018-compose-screenshot-testing/research.md
new file mode 100644
index 0000000000..daf5b760fc
--- /dev/null
+++ b/specs/018-compose-screenshot-testing/research.md
@@ -0,0 +1,96 @@
+# Research: Compose Preview Screenshot Testing
+
+**Feature**: 018-compose-screenshot-testing
+**Date**: 2026-05-08
+
+## 1. CST Plugin Compatibility
+
+**Decision**: Use `com.android.compose.screenshot` version `0.0.1-alpha14`.
+
+**Rationale**: Alpha14 (released 2026-04-08) is the latest stable alpha. It supports AGP 9.0+ (project uses 9.2.1), Kotlin 2.2.10+ (project uses 2.3.21), and JDK 17-24 (project uses 21). Alpha12 introduced AGP 9.x support; alpha14 includes critical multi-module bug fixes.
+
+**Alternatives considered**:
+- Roborazzi β user explicitly rejected; wants official Google tooling
+- Paparazzi β user explicitly rejected; same reason
+- Older CST alphas β no reason to use older versions; alpha14 has the best AGP 9.x support
+
+## 2. CMP @Preview Annotation Compatibility
+
+**Decision**: Use existing `androidx.compose.ui.tooling.preview.Preview` annotations from CMP 1.11+ `commonMain` composables directly β no wrapper annotations needed.
+
+**Rationale**: CMP 1.11 relocated `@Preview` to the `androidx.compose.ui.tooling.preview` package β identical to Jetpack Compose's package. The CST plugin scans for exactly this annotation. Existing `commonMain` previews are already compatible.
+
+**Alternatives considered**:
+- Duplicating previews with Jetpack-specific imports β unnecessary since CMP 1.11+ uses the same package
+- Using `org.jetbrains.compose.ui.tooling.preview.Preview` β deprecated in CMP 1.11+
+
+## 3. @PreviewTest Annotation
+
+**Decision**: Use `@PreviewTest` from `com.android.tools.screenshot:screenshot-validation-api:0.0.1-alpha14` in `screenshotTestImplementation`.
+
+**Rationale**: Since alpha10, all preview functions in the `screenshotTest` source set MUST be annotated with `@PreviewTest` β unannotated `@Preview` functions are skipped. The annotation is provided by the `screenshot-validation-api` artifact.
+
+**Key detail**: `@PreviewTest` composables can call composables from other modules. The `screenshotTest` source set has visibility to all `implementation` dependencies.
+
+## 4. Experimental Flags
+
+**Decision**: Set both the `gradle.properties` flag AND the module-level `experimentalProperties` flag.
+
+**Rationale**: Both flags are still required in alpha14:
+- `gradle.properties`: `android.experimental.enableScreenshotTest=true`
+- Module `build.gradle.kts`: `experimentalProperties["android.experimental.enableScreenshotTest"] = true`
+
+## 5. Module Architecture
+
+**Decision**: Create a dedicated Android-only module `screenshot-tests/` (not inside `core/` or `feature/`). Apply `com.android.library` + `com.android.compose.screenshot` + `kotlin-android`.
+
+**Rationale**: The CST plugin is Android-only β it cannot be applied to KMP modules. The official docs state: "Both the IDE and the underlying plugin are engineered exclusively for Android projects." A dedicated Android-only module that depends on KMP modules and imports their `commonMain` composables is the clean workaround.
+
+**Alternatives considered**:
+- Adding CST to the `app` module β couples screenshot tests to the app module; muddies separation
+- Adding CST to individual KMP modules β impossible; plugin doesn't support non-Android targets
+- Using `core:api` or `core:barcode` as a pattern β those are `com.android.library` modules but serve different purposes
+
+## 6. Reference Image Management
+
+**Decision**: Store reference images in `screenshot-tests/src/screenshotTestDebug/reference/` (default CST path) and commit to Git.
+
+**Rationale**: Reference images must be version-controlled for PR review. The default path follows the CST convention. Set `imageDifferenceThreshold = 0.0005f` (0.05%) to absorb minor JDK font rendering differences between macOS and Linux CI.
+
+## 7. Preview Visibility Audit
+
+**Decision**: Upgrade `private` preview composables to `internal` in modules that need screenshot test coverage.
+
+**Rationale**: Most existing previews are `private`. The `screenshot-tests` module cannot import private functions from other modules. Changing to `internal` preserves encapsulation (visible only within the module's compilation unit) while enabling cross-module screenshot testing.
+
+**Modules requiring audit**:
+- `feature/node/detail/NodeDetailPreviews.kt` β 4 previews are explicitly `private`
+- Many `core/ui/component/*.kt` previews lack explicit visibility (default `public`) but need theme-wrapping consistency review
+
+## 8. CI Integration
+
+**Decision**: Add a screenshot validation step to the `pull-request.yml` workflow. Run `validateFdroidDebugScreenshotTest`. Upload HTML diff report as artifact on failure.
+
+**Rationale**: The CST plugin runs entirely on the host JVM (no emulator needed), making it fast for CI. The `fdroidDebug` variant aligns with the project's primary OSS flavor. The HTML diff report includes side-by-side reference/actual/diff images.
+
+**Alternatives considered**:
+- Running both `fdroid` and `google` variants β unnecessary cost; UI is identical across flavors for most components
+- Running on a separate workflow β adds complexity without benefit; better as a step in existing PR checks
+
+## 9. Docs Pipeline Integration
+
+**Decision**: Create a `copyDocsScreenshots` Gradle task that copies selected reference images to a docs asset directory.
+
+**Rationale**: Spec 003 (app-docs-markdown) requires screenshot assets from automation. Rather than a separate screenshot tool, reuse the CST-generated reference images. The task selects docs-relevant images by naming convention or manifest file.
+
+## 10. Convention Plugin Usage
+
+**Decision**: Do NOT use existing `meshtastic.kmp.*` convention plugins for the `screenshot-tests` module. Use `meshtastic.detekt` and `meshtastic.spotless` only. Apply `com.android.library` and CST plugin directly.
+
+**Rationale**: The `screenshot-tests` module is intentionally Android-only. KMP convention plugins would add unnecessary multiplatform machinery. The existing `AndroidLibraryConventionPlugin` could be referenced but the screenshot module has unique requirements (CST plugin, experimental flags) that don't fit the existing pattern cleanly.
+
+## 11. Feature Modules Without Preview Coverage
+
+**Decision**: Prioritize adding previews to `feature/connections`, `feature/settings` (commonMain), and `feature/firmware`. Defer `feature/map` (heavy platform-specific rendering) and `feature/widget` (Glance, not standard Compose).
+
+**Rationale**: `feature/connections`, `feature/settings`, and `feature/firmware` have standard Compose UI in `commonMain` that is straightforward to preview. `feature/map` relies on platform-specific map renderers that don't work in preview context. `feature/widget` uses Glance composables which are incompatible with CST.
diff --git a/specs/018-compose-screenshot-testing/spec.md b/specs/018-compose-screenshot-testing/spec.md
new file mode 100644
index 0000000000..ea222f93b9
--- /dev/null
+++ b/specs/018-compose-screenshot-testing/spec.md
@@ -0,0 +1,144 @@
+# Feature Specification: Compose Preview Screenshot Testing
+
+**Feature Branch**: `018-compose-screenshot-testing`
+**Created**: 2026-05-08
+**Status**: Draft
+**Input**: User description: "Set up official Compose Preview Screenshot Testing, add comprehensive CMP previews across modules, and leverage generated screenshots in the automated docs feature."
+
+## User Scenarios & Testing *(mandatory)*
+
+### User Story 1 β Catch Visual Regressions Automatically (Priority: P1)
+
+A contributor changes a shared UI component (for example, the message bubble composable in `feature/messaging`). When they run the local verification command or push a PR, the screenshot validation task detects that the rendered output differs from the approved reference image and fails, surfacing the exact visual diff before the change can merge.
+
+**Why this priority**: Without regression detection the entire feature has no value. This is the foundational capability that all other stories build on.
+
+**Independent Test**: Modify a composable's padding or color, run the validate task, and confirm the task fails with a diff report showing the change.
+
+**Acceptance Scenarios**:
+
+1. **Given** reference images exist for a previewed component, **When** a contributor changes that component's layout or styling, **Then** the validate task fails and produces an HTML report highlighting the pixel differences.
+2. **Given** the contributor intentionally changed the UI, **When** they run the update task, **Then** new reference images replace the old ones and the validate task passes on the next run.
+3. **Given** no UI changes were made, **When** the validate task runs, **Then** it passes without producing diff artifacts.
+
+---
+
+### User Story 2 β Add and Maintain Preview Composables in commonMain (Priority: P2)
+
+A contributor wants to add a new preview for a component they built in a KMP feature module. They create or enhance a `*Previews.kt` file in `commonMain` with `@Preview` or `@PreviewLightDark`, see the preview in the IDE, and then add a thin `@PreviewTest` wrapper in the screenshot test module so the preview is included in automated screenshot validation.
+
+**Why this priority**: Previews are the input to the screenshot testing pipeline. Without comprehensive coverage, regression detection is limited to a handful of components.
+
+**Independent Test**: Create a new `@PreviewLightDark` composable in a feature module's `commonMain`, add a `@PreviewTest` wrapper in the screenshot test module, run the update task, and verify that reference images are generated for both light and dark variants.
+
+**Acceptance Scenarios**:
+
+1. **Given** a contributor creates a new `@PreviewLightDark` composable in `commonMain`, **When** they add a `@PreviewTest` wrapper in the screenshot test module and run the update task, **Then** reference images are generated for both light and dark theme variants.
+2. **Given** an existing preview uses `private` visibility, **When** the contributor upgrades it to `internal`, **Then** the screenshot test module can import and wrap it without other changes.
+3. **Given** a preview wraps its content in the project theme, **When** the reference image is generated, **Then** the image reflects the correct Material 3 theming and icon set.
+
+---
+
+### User Story 3 β Supply Screenshot Assets to the Docs Pipeline (Priority: P3)
+
+The automated docs feature (spec 003) needs screenshot assets that stay in sync with the codebase. A build step or CI task copies selected reference images from the screenshot test output into the docs asset directory so they can be embedded in the published documentation site and in-app help browser.
+
+**Why this priority**: Screenshots that drift from the actual UI erode user trust. Tying docs assets to the same validated reference images eliminates manual screenshot capture.
+
+**Independent Test**: Generate reference images, run the docs asset copy task, and verify the target directory contains the expected PNGs matching the current UI state.
+
+**Acceptance Scenarios**:
+
+1. **Given** reference images exist in the screenshot test module, **When** the docs asset sync task runs, **Then** selected images are copied to the docs asset directory with stable, predictable file names.
+2. **Given** a UI change updates a reference image, **When** the docs pipeline rebuilds, **Then** the published docs reflect the updated screenshot without manual intervention.
+3. **Given** a reference image is deleted or renamed, **When** the docs build runs, **Then** the build warns about missing expected assets rather than silently producing broken image links.
+
+---
+
+### User Story 4 β Validate Screenshots in CI (Priority: P4)
+
+When a pull request is opened against `main`, the CI workflow runs the screenshot validation task as part of the check suite. If any reference images are missing or differ from the rendered output, the check fails and the contributor is directed to review the HTML diff report or update references.
+
+**Why this priority**: CI enforcement ensures that visual regressions are caught even when contributors skip local validation.
+
+**Independent Test**: Open a PR that changes a previewed component without updating reference images. Confirm the CI check fails and the diff report artifact is uploaded.
+
+**Acceptance Scenarios**:
+
+1. **Given** a PR changes a previewed composable without updating references, **When** CI runs, **Then** the screenshot validation check fails.
+2. **Given** a PR includes updated reference images that match the new UI, **When** CI runs, **Then** the screenshot validation check passes.
+3. **Given** the screenshot validation task fails, **When** the CI workflow completes, **Then** the HTML diff report is uploaded as a build artifact for review.
+
+---
+
+### User Story 5 β Audit and Upgrade Existing Previews (Priority: P5)
+
+The project already has preview composables in several modules (`core/ui`, `feature/messaging`, `feature/node`). These need to be audited for visibility (must be `internal`, not `private`), theme wrapping (must use `AppTheme`), and sample data quality (must be synthetic). Modules without any previews need initial coverage for their primary components.
+
+**Why this priority**: Existing previews must meet the conventions before screenshot tests can reference them. New previews for uncovered modules expand regression coverage.
+
+**Independent Test**: Run the screenshot test suite after the audit and confirm that all existing previews generate valid reference images and that newly added previews for previously uncovered modules also produce references.
+
+**Acceptance Scenarios**:
+
+1. **Given** an existing preview uses `private` visibility, **When** the audit upgrades it to `internal`, **Then** the screenshot test module can import it without other code changes.
+2. **Given** a feature module has no previews, **When** previews are added for its primary components, **Then** reference images are generated and the module's UI is covered by regression detection.
+3. **Given** all audited and new previews are wrapped in `AppTheme`, **When** reference images are generated, **Then** they consistently reflect the project's Material 3 theme in both light and dark variants.
+
+---
+
+### Edge Cases
+
+- What happens when a preview composable references a CompositionLocal that is not provided in the screenshot test context? The preview must supply its own defaults or use `LocalInspectionMode` guards.
+- What happens when a preview depends on Compose resources that are not available in the `screenshotTest` source set? The screenshot test module must depend on the module that owns the resources.
+- What happens when the CST plugin version is incompatible with the current AGP version? The build fails at configuration time with a clear error; the version catalog entry makes the version easy to update.
+- What happens when a contributor adds a `@PreviewTest` wrapper but forgets to generate reference images? The validate task fails because no reference exists for the new test, prompting the contributor to run the update task.
+- What happens when reference images differ across operating systems (macOS vs Ubuntu CI)? Both recording and validation should run on the same OS (CI uses `ubuntu-24.04`); contributors regenerate references via CI or accept minor rendering differences with a documented threshold.
+
+## Requirements *(mandatory)*
+
+### Functional Requirements
+
+- **FR-001**: The project MUST integrate the official Compose Preview Screenshot Testing plugin (`com.android.compose.screenshot`) at version 0.0.1-alpha14 or later.
+- **FR-002**: The `screenshotTest` source set MUST live in a dedicated Android-only module (`screenshot-tests/`) rather than in the `app` module or in KMP modules.
+- **FR-003**: The `screenshot-tests` module MUST depend on KMP feature and core modules so it can import their `commonMain` preview composables.
+- **FR-004**: Preview composables referenced by screenshot tests MUST have `internal` or `public` visibility (not `private`).
+- **FR-005**: Preview composables MUST wrap their content in the project theme (`AppTheme`) and use synthetic sample data only β never real user data, PII, or location coordinates.
+- **FR-006**: Screenshot test wrappers MUST be annotated with `@PreviewTest` and the corresponding `@Preview` or `@PreviewLightDark` annotation so the CST plugin knows what configurations to render.
+- **FR-007**: The version catalog (`gradle/libs.versions.toml`) MUST include entries for the CST plugin and the `screenshot-validation-api` library.
+- **FR-008**: The `android.experimental.enableScreenshotTest=true` property MUST be set in `gradle.properties`.
+- **FR-009**: The `screenshot-tests` module MUST be included in `settings.gradle.kts`.
+- **FR-010**: The `screenshot-tests` module MUST apply `meshtastic.detekt` and `meshtastic.spotless` convention plugins so screenshot test code passes the same static analysis gates as all other code.
+- **FR-011**: Existing preview composables in `core/ui`, `feature/messaging`, and `feature/node` MUST be audited and upgraded to meet the visibility and theming conventions.
+- **FR-012**: New preview composables MUST be added for primary components in `feature/connections`, `feature/settings`, `feature/firmware`, `feature/intro` (commonMain portions), and `core/ui` (shared components, icons, indicators). Modules without standard Compose UI composables (`feature/metrics`, `feature/auto`, `feature/discovery`, `feature/docs`) and modules relying on platform-specific renderers (`feature/map`, `feature/widget`) are excluded.
+- **FR-013**: A Gradle task or script MUST exist to copy selected reference images from the screenshot test output into a docs asset directory for consumption by the docs feature (spec 003).
+- **FR-014**: The CI workflow MUST include a screenshot validation step that runs `validateFdroidDebugScreenshotTest` and uploads the HTML diff report as a build artifact on failure.
+- **FR-015**: Reference images MUST be stored in version control so they are reviewable in pull request diffs.
+- **FR-016**: All user-visible strings used in preview sample data MUST use string resources from `core/resources` or hardcoded synthetic placeholders β never production user data.
+- **FR-017**: The screenshot test module's `build.gradle.kts` MUST enable the experimental screenshot test flag in the `android {}` block.
+
+### Key Entities
+
+- **Preview Composable**: A `@Composable` function annotated with `@Preview` or `@PreviewLightDark` in a module's `commonMain` source set. Serves as both an IDE preview and screenshot test input.
+- **Screenshot Test Wrapper**: A `@Composable` function annotated with `@PreviewTest` in the `screenshotTest` source set that calls a preview composable.
+- **Reference Image**: A PNG file generated by the CST update task, stored in `screenshotTestDebug/reference/`, used as the ground truth for validation.
+
+## Success Criteria *(mandatory)*
+
+### Measurable Outcomes
+
+- **SC-001**: The screenshot validation task passes when no UI components have changed and fails within 60 seconds when a previewed component's appearance changes.
+- **SC-002**: At least 80% of eligible modules have at least one preview composable covered by a screenshot test. Eligible modules are those containing standard Compose UI composables in `commonMain`: `core:ui`, `feature:messaging`, `feature:node`, `feature:wifi-provision`, `feature:connections`, `feature:settings`, `feature:firmware`, `feature:intro` (8 modules; threshold = 7).
+- **SC-003**: The CI pipeline includes screenshot validation as a required check, and the HTML diff report is available as an artifact when validation fails.
+- **SC-004**: Reference images for docs-relevant components are consumable by the docs build pipeline (spec 003) without manual screenshot capture.
+- **SC-005**: A new contributor can add a preview and its screenshot test wrapper by following the quickstart guide in under 10 minutes.
+- **SC-006**: All screenshot test code passes `spotlessCheck` and `detekt` without new violations.
+
+## Assumptions
+
+- The official Compose Preview Screenshot Testing plugin (0.0.1-alpha14) is compatible with AGP 9.2.1 and Kotlin 2.3.21, which the project already uses.
+- The CST plugin does not support KMP module targets directly; the workaround of a dedicated Android-only module is the recommended approach for KMP projects.
+- CMP 1.11+ `@Preview` in `commonMain` is the standard annotation supported by the CST plugin when composables are consumed from an Android module.
+- Reference images may differ slightly between macOS (local dev) and Ubuntu (CI). The project will standardize on CI-generated references or accept a documented comparison threshold.
+- The docs feature (spec 003) will define the exact directory and naming convention for screenshot assets it consumes; this spec provides the generation and copy mechanism.
+- Preview composables do not require ViewModel, DI, or network access β they render stateless UI with synthetic data only.
diff --git a/specs/018-compose-screenshot-testing/tasks.md b/specs/018-compose-screenshot-testing/tasks.md
new file mode 100644
index 0000000000..136926a035
--- /dev/null
+++ b/specs/018-compose-screenshot-testing/tasks.md
@@ -0,0 +1,192 @@
+# Tasks: Compose Preview Screenshot Testing
+
+**Input**: Design documents from `specs/018-compose-screenshot-testing/`
+**Prerequisites**: plan.md, spec.md, research.md, data-model.md, quickstart.md
+
+## Format: `[ID] [P?] [Story?] Description`
+
+- **[P]**: Can run in parallel (different files, no dependencies)
+- **[Story]**: Which user story this task belongs to (e.g., US1, US2)
+- Exact file paths included in descriptions
+
+---
+
+## Phase 1: Setup (Shared Infrastructure)
+
+**Purpose**: Add CST plugin to the build system, create the screenshot-tests module, and configure experimental flags.
+
+- [x] T001 [P] Add CST plugin and screenshot-validation-api entries to `gradle/libs.versions.toml` (version `compose-screenshot = "0.0.1-alpha14"`, library `screenshot-validation-api`, plugin `compose-screenshot`)
+- [x] T002 [P] Add `android.experimental.enableScreenshotTest=true` to `gradle.properties`
+- [x] T003 Create `screenshot-tests/build.gradle.kts` with `com.android.library`, `com.android.compose.screenshot`, `kotlin-android`, compose compiler, `meshtastic.detekt`, `meshtastic.spotless` plugins; configure `namespace`, `compileSdk`, `minSdk`, `experimentalProperties`, `imageDifferenceThreshold`, and `screenshotTestImplementation` dependencies
+- [x] T004 [P] Create minimal `screenshot-tests/src/main/AndroidManifest.xml`
+- [x] T005 Add `include(":screenshot-tests")` to `settings.gradle.kts` (conditionally excluded when `desktopOnly`)
+- [x] T006 Verify the module compiles: run `./gradlew :screenshot-tests:assembleDebug`
+
+**Checkpoint**: The screenshot-tests module exists, compiles, and is ready to receive `@PreviewTest` wrappers.
+
+---
+
+## Phase 2: Foundational (Blocking Prerequisites)
+
+**Purpose**: Ensure core/ui previews meet conventions (visibility, theme wrapping) so screenshot test wrappers can import them.
+
+**CRITICAL**: These audit tasks must complete before US1 can validate any screenshots.
+
+- [x] T007 Audit `core/ui/src/commonMain/` preview composables: inventory all `@Preview`/`@PreviewLightDark` functions, record visibility and `AppTheme` wrapping status
+- [x] T008 [P] Fix `core/ui` previews lacking `AppTheme` wrapping β add `AppTheme { ... }` wrapper to preview functions in `EditListPreference.kt`, `SwitchPreference.kt`, `ElevationInfo.kt`, `PreferenceCategory.kt`, `EditTextPreference.kt`, `SecurityIcon.kt`, `BitwisePreference.kt`, `PositionPrecisionPreference.kt`, `RegularPreference.kt`, `IndoorAirQuality.kt`, `EditBase64Preference.kt`, `DropDownPreference.kt`, `NodeChip.kt`, `TextDividerPreference.kt`, `EditIPv4Preference.kt`, `IconInfo.kt`, `LazyColumnDragAndDropDemo.kt`, `EditPasswordPreference.kt`
+- [x] T009 [P] Change `private` preview functions to `internal` in `feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/NodeDetailPreviews.kt` (4 functions)
+- [x] T010 [P] Verify all audited previews compile on all KMP targets: run `./gradlew :core:ui:compileKotlinJvm :feature:node:compileKotlinJvm`
+
+**Checkpoint**: All existing previews have `internal`/`public` visibility and `AppTheme` wrapping. Ready for screenshot test wrappers.
+
+---
+
+## Phase 3: User Story 1 β Catch Visual Regressions Automatically (Priority: P1) MVP
+
+**Goal**: Wire a representative set of existing previews into the screenshot test module, generate reference images, and validate that the regression detection pipeline works end-to-end.
+
+**Independent Test**: Modify a composable's padding or color, run `validateFdroidDebugScreenshotTest`, confirm it fails with a diff report.
+
+### Implementation for User Story 1
+
+- [x] T011 [US1] Add `implementation(project(":core:ui"))` and `implementation(project(":core:resources"))` dependencies to `screenshot-tests/build.gradle.kts`
+- [x] T012 [P] [US1] Create `screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/core/AlertScreenshotTests.kt` with `@PreviewTest` + `@Preview`/`@PreviewLightDark` wrappers for `AlertPreviews.kt` preview functions from `core:ui` (each wrapper must have BOTH `@PreviewTest` AND the matching preview annotation per FR-006)
+- [x] T013 [P] [US1] Create `screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/core/ComponentScreenshotTests.kt` with `@PreviewTest` + `@Preview`/`@PreviewLightDark` wrappers for a representative subset of `core:ui` component previews (`NodeKeyStatusIcon`, `ChannelItem`, `ListItem`, `HopsInfo`, `SignalInfo`, `DistanceInfo`, `LastHeardInfo`, `MaterialBatteryInfo`, `TitledCard`, `ChannelInfo`, `SatelliteCountInfo`, `MaterialBluetoothSignalInfo`) β each wrapper must have BOTH annotations per FR-006
+- [x] T014 [US1] Generate initial reference images: run `./gradlew :screenshot-tests:updateDebugScreenshotTest`
+- [x] T015 [US1] Validate screenshot tests pass: run `./gradlew :screenshot-tests:validateDebugScreenshotTest`
+- [x] T016 [US1] End-to-end regression test: temporarily modify a previewed composable's styling (e.g., padding in `ChannelItem.kt`), run validate task, confirm it fails with HTML diff report at `screenshot-tests/build/reports/screenshotTest/preview/fdroidDebug/index.html`, then revert the change
+- [x] T017 [US1] Commit reference images in `screenshot-tests/src/screenshotTestDebug/reference/` to version control
+
+**Checkpoint**: Visual regression detection works end-to-end for core/ui components. A UI change triggers a validate failure with a diff report.
+
+---
+
+## Phase 4: User Story 2 β Add and Maintain Preview Composables in commonMain (Priority: P2)
+
+**Goal**: Expand screenshot coverage to feature modules with existing previews and add new previews for uncovered modules.
+
+**Independent Test**: Create a new `@PreviewLightDark` composable in a feature module, add a `@PreviewTest` wrapper, run update task, verify reference images appear for both light and dark variants.
+
+### Implementation for User Story 2
+
+- [x] T018 [US2] Add feature module dependencies to `screenshot-tests/build.gradle.kts`: `implementation(project(":feature:messaging"))`, `implementation(project(":feature:node"))`, `implementation(project(":feature:wifi-provision"))`, `implementation(project(":feature:connections"))`, `implementation(project(":feature:settings"))`, `implementation(project(":feature:firmware"))`, `implementation(project(":feature:intro"))`
+- [x] T019 [P] [US2] Create `screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/feature/MessagingScreenshotTests.kt` with `@PreviewTest` wrappers for previews from `feature/messaging` (`QuickChatPreviews`, `MessageItemPreviews`, `ReactionPreviews`)
+- [x] T020 [P] [US2] Create `screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/feature/NodeScreenshotTests.kt` with `@PreviewTest` wrappers for previews from `feature/node` (`NodeDetailComponentPreviews`, `NodeDetailPreviews`, `EnvironmentMetrics`, `DeviceMetrics`, `CommonCharts`)
+- [x] T021 [P] [US2] Create `screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/feature/WifiProvisionScreenshotTests.kt` with `@PreviewTest` wrappers for previews from `feature/wifi-provision` (`WifiProvisionPreviews`)
+- [x] T022 [P] [US2] Add new preview composables in `feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/component/ConnectionsPreviews.kt` for `DeviceListItem`, `ConnectingDeviceInfo`, `DisconnectButton` with `@PreviewLightDark`, `internal` visibility, `AppTheme` wrapping, and synthetic sample data
+- [x] T023 [P] [US2] Add new preview composables in `feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/component/SettingsPreviews.kt` for primary settings sections with `@PreviewLightDark`, `internal` visibility, `AppTheme` wrapping (migrate from existing `androidMain` previews where possible)
+- [x] T024 [P] [US2] Add new preview composables in `feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwarePreviews.kt` for `FirmwareUpdateScreen` and sub-components with `@PreviewLightDark`, `internal` visibility, `AppTheme` wrapping
+- [x] T024a [P] [US2] Add new preview composables in `feature/intro/src/commonMain/kotlin/org/meshtastic/feature/intro/IntroPreviews.kt` for welcome/onboarding screen composables with `@PreviewLightDark`, `internal` visibility, `AppTheme` wrapping (migrate from existing `androidMain` preview where possible)
+- [x] T025 [P] [US2] Create `screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/feature/ConnectionsScreenshotTests.kt` with `@PreviewTest` wrappers for the new connections previews
+- [x] T026 [P] [US2] Create `screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/feature/SettingsScreenshotTests.kt` with `@PreviewTest` wrappers for the new settings previews
+- [x] T027 [P] [US2] Create `screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/feature/FirmwareScreenshotTests.kt` with `@PreviewTest` wrappers for the new firmware previews
+- [x] T027a [P] [US2] Create `screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/feature/IntroScreenshotTests.kt` with `@PreviewTest` wrappers for the new intro previews
+- [x] T028 [US2] Generate reference images for all new wrappers: run `./gradlew :screenshot-tests:updateFdroidDebugScreenshotTest`
+- [x] T029 [US2] Validate all screenshot tests pass: run `./gradlew :screenshot-tests:validateFdroidDebugScreenshotTest`
+- [x] T030 [US2] Commit updated reference images to version control
+
+**Checkpoint**: Feature modules have preview coverage. New previews for `connections`, `settings`, and `firmware` exist in `commonMain`. All generate valid reference images.
+
+---
+
+## Phase 5: User Story 3 β Supply Screenshot Assets to Docs Pipeline (Priority: P3)
+
+**Goal**: Create a Gradle task that copies selected reference images to a docs asset directory for consumption by spec 003.
+
+**Independent Test**: Run the docs copy task, verify the target directory contains expected PNGs.
+
+### Implementation for User Story 3
+
+- [x] T031 [US3] Create a `copyDocsScreenshots` Gradle task in `screenshot-tests/build.gradle.kts` that copies selected reference images from `src/screenshotTestDebug/reference/` to `docs/screenshots/` with stable file names (based on a manifest or naming convention filter)
+- [x] T032 [US3] Create `screenshot-tests/docs-screenshots-manifest.txt` listing the reference image patterns to copy (one per line, supports glob patterns)
+- [x] T033 [US3] Verify the task works: run `./gradlew :screenshot-tests:copyDocsScreenshots` and confirm `docs/screenshots/` contains the expected PNGs
+- [x] T034 [US3] Add `docs/screenshots/` to `.gitignore` (generated output, not committed β the docs build regenerates from references)
+
+**Checkpoint**: The docs pipeline can consume screenshot assets via the `copyDocsScreenshots` task.
+
+---
+
+## Phase 6: User Story 4 β Validate Screenshots in CI (Priority: P4)
+
+**Goal**: Add screenshot validation to the CI pipeline so regressions are caught on every PR.
+
+**Independent Test**: Open a PR that changes a previewed component without updating references; confirm CI fails and uploads the diff report.
+
+### Implementation for User Story 4
+
+- [x] T035 [US4] Add screenshot validation step to `.github/workflows/pull-request.yml`: run `./gradlew :screenshot-tests:validateFdroidDebugScreenshotTest` after the existing build step
+- [x] T036 [US4] Add artifact upload step to `.github/workflows/pull-request.yml`: on validation failure, upload `screenshot-tests/build/reports/screenshotTest/` as `screenshot-diff-report` artifact
+- [x] T037 [US4] If `pull-request.yml` delegates to `reusable-check.yml`, add the screenshot validation and artifact upload steps to the reusable workflow instead
+- [x] T038 [US4] Verify CI integration by reviewing the workflow YAML for correctness (step ordering, `if: failure()` condition on artifact upload, correct Gradle task name)
+
+**Checkpoint**: CI runs screenshot validation on every PR. Diff reports are uploaded as artifacts on failure.
+
+---
+
+## Phase 7: User Story 5 β Audit and Upgrade Existing Previews (Priority: P5)
+
+**Goal**: Complete the audit of remaining core/ui component previews and add remaining `@PreviewTest` wrappers for full coverage.
+
+**Independent Test**: Run the full screenshot test suite and confirm all existing and new previews generate valid reference images.
+
+### Implementation for User Story 5
+
+- [x] T039 [P] [US5] Create `@PreviewTest` wrappers for remaining `core:ui` component previews not covered in T013: `ImportFab`, `SliderPreference`, `EditTextPreference`, `EditListPreference`, `SwitchPreference`, `BitwisePreference`, `PositionPrecisionPreference`, `RegularPreference`, `DropDownPreference`, `EditBase64Preference`, `EditIPv4Preference`, `EditPasswordPreference`, `PreferenceCategory`, `TextDividerPreference`, `ElevationInfo`, `SecurityIcon`, `IndoorAirQuality`, `NodeChip`, `IconInfo` in `screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/core/PreferenceComponentScreenshotTests.kt`
+- [x] T040 [P] [US5] Verify `feature:messaging` previews use `AppTheme` wrapping consistently; fix any that don't in `feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/Message.kt` and `feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/ReactionPreviews.kt`
+- [x] T041 [P] [US5] Verify `feature:wifi-provision` previews use `AppTheme` wrapping consistently; fix any that don't in `feature/wifi-provision/src/commonMain/kotlin/org/meshtastic/feature/wifiprovision/ui/WifiProvisionPreviews.kt`
+- [x] T042 [US5] Generate final reference images for all wrappers: run `./gradlew :screenshot-tests:updateDebugScreenshotTest`
+- [x] T043 [US5] Validate complete screenshot test suite passes: run `./gradlew :screenshot-tests:validateDebugScreenshotTest`
+- [x] T044 [US5] Commit all final reference images to version control (pending β will commit with Phase 8)
+
+**Checkpoint**: All existing previews meet conventions. Full coverage achieved across audited modules.
+
+---
+
+## Phase 8: Polish & Cross-Cutting Concerns
+
+**Purpose**: Verification, documentation, and quality gates.
+
+- [x] T045 [P] Update `specs/018-compose-screenshot-testing/quickstart.md` with any corrections discovered during implementation
+- [x] T046 [P] Confirm no logs, telemetry, or config changes expose PII, location data, secrets, or modify `core/proto`
+- [x] T047 [P] Run constitution-required verification: `./gradlew spotlessApply spotlessCheck detekt assembleDebug test allTests :screenshot-tests:validateDebugScreenshotTest`
+- [x] T048 [P] Verify `screenshot-tests` module lint passes: `./gradlew :screenshot-tests:detekt`
+- [x] T049 Validate quickstart guide: follow the steps in `specs/018-compose-screenshot-testing/quickstart.md` to add a new preview + screenshot test from scratch, confirm it works end-to-end
+
+---
+
+## Dependencies & Execution Order
+
+### Phase Dependencies
+
+- **Setup (Phase 1)**: No dependencies β can start immediately
+- **Foundational (Phase 2)**: Depends on Phase 1 (T003 must exist for T010 compile check) β BLOCKS all user stories
+- **US1 (Phase 3)**: Depends on Phase 2 completion β MVP deliverable
+- **US2 (Phase 4)**: Depends on Phase 1 (module exists) + Phase 2 (previews audited). Can start in parallel with US1 for new preview authoring (T022-T024), but wrapper creation (T019-T021, T025-T027) depends on US1 confirming the pipeline works
+- **US3 (Phase 5)**: Depends on US1 (reference images must exist). Independent of US2
+- **US4 (Phase 6)**: Depends on US1 (validation task must work). Independent of US2 and US3
+- **US5 (Phase 7)**: Depends on Phase 2 (audit started) + US1 (pipeline validated). Can run after US1
+- **Polish (Phase 8)**: Depends on all desired user stories being complete
+
+### User Story Dependencies
+
+- **US1 (P1)**: Core MVP β validate regression detection works. No dependencies on other stories
+- **US2 (P2)**: Expands coverage. New preview authoring (T022-T024) can start after Phase 2; wrappers and reference generation after US1
+- **US3 (P3)**: Docs integration. Only needs reference images from US1. Independent of US2, US4, US5
+- **US4 (P4)**: CI integration. Only needs working validate task from US1. Independent of US2, US3, US5
+- **US5 (P5)**: Coverage completion. Extends US1's initial coverage to all remaining previews
+
+### Parallel Opportunities
+
+- **Phase 1**: T001, T002, T004 can run in parallel (different files)
+- **Phase 2**: T008, T009, T010 can run in parallel (different modules/files)
+- **Phase 3**: T012, T013 can run in parallel (different test files)
+- **Phase 4**: T019-T027 can all run in parallel (different test/preview files)
+- **Phase 6**: T035, T036 can run in parallel if in same file (workflow steps are additive)
+- **Phase 7**: T039, T040, T041 can run in parallel (different files)
+- **Phase 8**: T045-T048 can all run in parallel
+
+### Implementation Strategy
+
+- **MVP**: Phases 1-3 (Setup + Foundational + US1). Delivers a working screenshot regression pipeline for core/ui components
+- **Incremental**: Phase 4 (US2) expands coverage. Phase 5 (US3) adds docs integration. Phase 6 (US4) adds CI enforcement
+- **Full**: Phase 7 (US5) completes coverage audit. Phase 8 polishes and validates
Served by rngit 1.5.4 - Generated in 1.66s